Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled

Layout:
- src/                    .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
                          MxAsbClient, probes, tests, harnesses. Executable spec.
- design/                 Architectural plan for the Rust port (M0–M6), error
                          model, protocol invariants, risks (R1–R16), adversarial
                          review log (review.md).
- rust/                   Rust workspace. M0 skeleton + M1 codec parity.
                          mxaccess-codec: 215 unit tests + 2 cross-implementation
                          parity tests (byte-identical against .NET reference).
                          Other crates are M0 stubs awaiting M2+.
- captures/               Frida + netsh + pcap evidence per CLAUDE.md
                          ("captures are evidence, not throwaway logs").
- analysis/               Decompiled C# (frida/proxy/decompiled-*),
                          Ghidra exports for native DLLs (`exports/` only —
                          working state at `projects/` and AVEVA's input
                          binaries at `input/` are gitignored).
- docs/                   Reverse-engineering reference docs.
- tools/                  Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
                          Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/      Rust CI: fmt + build + test + clippy on Windows.
- LICENSE                 MIT (Joseph Doherty, 2026).

Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly

Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 06:21:00 -04:00
parent 43733699b0
commit fe2a6db786
3849 changed files with 352975 additions and 0 deletions
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\MxAsbClient\MxAsbClient.csproj" />
</ItemGroup>
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
</Project>
+830
View File
@@ -0,0 +1,830 @@
using MxAsbClient;
using System.Globalization;
string endpoint = GetArg(args, "--endpoint")
?? "net.tcp://desktop-6jl3kko/ASBService/Default_ZB_MxDataProvider/IDataV2";
string[] tags = GetArgs(args, "--tag");
if (tags.Length == 0)
{
tags = ["TestChildObject.TestInt"];
}
string tag = tags[0];
string? solution = GetArg(args, "--solution");
bool dumpMessages = HasArg(args, "--dump-messages");
bool subscribe = HasArg(args, "--subscribe");
bool subscribeBuffered = HasArg(args, "--subscribe-buffered");
bool compatibilitySubscribe = HasArg(args, "--compat-subscribe");
bool probeConnectFailure = HasArg(args, "--probe-connect-failure");
bool probeReconnect = HasArg(args, "--probe-reconnect");
bool probeCanceledCleanup = HasArg(args, "--probe-canceled-cleanup");
bool probeOperationCompleteCandidates = HasArg(args, "--probe-operation-complete-candidates");
int publishCount = TryGetInt(args, "--publish-count") ?? 3;
int subscribeSampleMs = TryGetInt(args, "--subscribe-sample-ms") ?? 1000;
int publishDelayMs = TryGetInt(args, "--publish-delay-ms") ?? 500;
int reconnectAttempts = TryGetInt(args, "--reconnect-attempts") ?? 2;
int reconnectDelayMs = TryGetInt(args, "--reconnect-delay-ms") ?? 250;
int cleanupDisconnectTimeoutMs = TryGetInt(args, "--cleanup-disconnect-timeout-ms") ?? 30000;
int cleanupCloseTimeoutMs = TryGetInt(args, "--cleanup-close-timeout-ms") ?? 30000;
bool waitWriteComplete = HasArg(args, "--wait-write-complete");
int writeCompleteTimeoutMs = TryGetInt(args, "--write-complete-timeout-ms") ?? 5000;
int writeCompletePollMs = TryGetInt(args, "--write-complete-poll-ms") ?? 250;
int writeReadbackDelayMs = TryGetInt(args, "--write-readback-delay-ms") ?? 0;
Variant? writeVariant = GetWriteVariant(args);
bool probeErrorCases = HasArg(args, "--probe-error-cases");
bool probeInvalidTargets = probeErrorCases || HasArg(args, "--probe-invalid-targets");
bool probeWrongTypeWrite = probeErrorCases || HasArg(args, "--probe-wrong-type-write");
bool probeInvalidCleanup = probeErrorCases || HasArg(args, "--probe-invalid-cleanup");
bool probeEmptyPublish = probeErrorCases || HasArg(args, "--probe-empty-publish");
string invalidTag = GetArg(args, "--invalid-tag") ?? "DefinitelyMissingObject.DefinitelyMissingAttribute";
string wrongTypeWriteTag = GetArg(args, "--wrong-type-write-tag") ?? tag;
int errorPublishCount = TryGetInt(args, "--error-publish-count") ?? 2;
int errorWriteCompleteTimeoutMs = TryGetInt(args, "--error-write-complete-timeout-ms") ?? 2000;
int errorWriteCompletePollMs = TryGetInt(args, "--error-write-complete-poll-ms") ?? 250;
Console.WriteLine($"process=x64:{Environment.Is64BitProcess}");
Console.WriteLine($"endpoint={endpoint}");
Console.WriteLine($"tags={string.Join(",", tags)}");
if (args.Any(arg => arg.Equals("--dump-register-payload", StringComparison.OrdinalIgnoreCase)))
{
byte[] payload = AsbPayloadDebug.SerializeItemsForDebug(tag);
Console.WriteLine($"register_payload_len={payload.Length}");
Console.WriteLine($"register_payload_b64={Convert.ToBase64String(payload)}");
return;
}
if (probeConnectFailure)
{
try
{
using MxAsbDataClient connectFailureClient = MxAsbDataClient.Connect(endpoint, solution, Console.WriteLine, dumpMessages);
Console.WriteLine("connect_failure_observed=False");
}
catch (Exception ex)
{
Console.WriteLine("connect_failure_observed=True");
Console.WriteLine($"connect_failure_exception={FormatException(ex)}");
if (ex.InnerException is not null)
{
Console.WriteLine($"connect_failure_inner_exception={FormatException(ex.InnerException)}");
}
}
return;
}
if (compatibilitySubscribe)
{
RunCompatibilitySubscribe(endpoint, solution, tags, dumpMessages, publishCount, subscribeSampleMs, publishDelayMs);
return;
}
using MxAsbDataClient client = MxAsbDataClient.Connect(endpoint, solution, Console.WriteLine, dumpMessages);
int publishedEventCount = 0;
client.PublishedValueReceived += (_, value) =>
{
publishedEventCount++;
Console.WriteLine($"published_event[{publishedEventCount - 1}]=item:{value.ItemName ?? string.Empty} id:{value.ItemId} type:{value.VariantType} quality:{FormatNullableHex(value.Quality)} timestamp:{value.TimestampUtc:O} preview:{value.Preview}");
};
Console.WriteLine("connect=True");
RegisterItemsResponse register = client.RegisterMany(tags);
Console.WriteLine($"register_error=0x{register.Result.ErrorCode:X8} status=0x{register.Result.Status:X8} specific=0x{register.Result.SpecificErrorCode:X8}");
PrintStatuses("register_status", register.Status);
ItemIdentity[] registeredItems = register.Status?.Select(status => status.Item).ToArray() ?? [];
IReadOnlyDictionary<ulong, string> itemNamesById = AsbPublishMapper.CreateItemNameMap(register.Status);
ReadResponse read = client.ReadMany(tags);
Console.WriteLine($"read_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("read_status", read.Status);
PrintValues("read_value", read.Values);
if (probeReconnect)
{
AsbReconnectResult reconnect = client.Reconnect(new AsbReconnectOptions
{
MaxAttempts = reconnectAttempts,
Delay = TimeSpan.FromMilliseconds(reconnectDelayMs),
CleanupOptions = new AsbClientCleanupOptions
{
DisconnectTimeout = TimeSpan.FromMilliseconds(cleanupDisconnectTimeoutMs),
CloseTimeout = TimeSpan.FromMilliseconds(cleanupCloseTimeoutMs),
},
});
Console.WriteLine($"reconnect_succeeded={reconnect.Succeeded} attempts={reconnect.Attempts.Count}");
PrintCleanup("reconnect_cleanup", reconnect.CleanupResult);
for (int i = 0; i < reconnect.Attempts.Count; i++)
{
AsbReconnectAttempt attempt = reconnect.Attempts[i];
Console.WriteLine($"reconnect_attempt[{i}]=attempt:{attempt.Attempt} succeeded:{attempt.Succeeded} exception:{attempt.Exception?.GetType().Name ?? string.Empty}");
}
if (reconnect.Client is not null)
{
using MxAsbDataClient reconnectedClient = reconnect.Client;
RegisterItemsResponse reconnectRegister = reconnectedClient.RegisterMany(tags);
Console.WriteLine($"reconnect_register_error=0x{reconnectRegister.Result.ErrorCode:X8} status=0x{reconnectRegister.Result.Status:X8} specific=0x{reconnectRegister.Result.SpecificErrorCode:X8}");
PrintStatuses("reconnect_register_status", reconnectRegister.Status);
ReadResponse reconnectRead = reconnectedClient.ReadMany(tags);
Console.WriteLine($"reconnect_read_error=0x{reconnectRead.Result.ErrorCode:X8} status=0x{reconnectRead.Result.Status:X8} specific=0x{reconnectRead.Result.SpecificErrorCode:X8}");
PrintStatuses("reconnect_read_status", reconnectRead.Status);
PrintValues("reconnect_read_value", reconnectRead.Values);
}
return;
}
if (probeCanceledCleanup)
{
AsbClientCleanupResult cleanup = client.Cleanup(new AsbClientCleanupOptions
{
DisconnectTimeout = TimeSpan.FromMilliseconds(cleanupDisconnectTimeoutMs),
CloseTimeout = TimeSpan.FromMilliseconds(cleanupCloseTimeoutMs),
CancellationToken = new CancellationToken(canceled: true),
});
PrintCleanup("canceled_cleanup", cleanup);
return;
}
if (probeInvalidTargets)
{
RunInvalidTargetProbe(client, invalidTag, errorWriteCompleteTimeoutMs, errorWriteCompletePollMs);
}
if (probeWrongTypeWrite)
{
RunWrongTypeWriteProbe(client, wrongTypeWriteTag, errorWriteCompleteTimeoutMs, errorWriteCompletePollMs);
}
if (probeInvalidCleanup)
{
RunInvalidCleanupProbe(client, subscribeSampleMs);
}
if (probeEmptyPublish)
{
RunEmptyPublishProbe(client, errorPublishCount, subscribeSampleMs, publishDelayMs);
}
if (probeOperationCompleteCandidates)
{
RunOperationCompleteCandidateProbe(client, tags, subscribeSampleMs, publishDelayMs);
return;
}
if (subscribe)
{
long subscriptionId = 0;
try
{
CreateSubscriptionResponse create = client.CreateSubscription(maxQueueSize: 128, sampleInterval: (ulong)subscribeSampleMs);
subscriptionId = create.SubscriptionId;
Console.WriteLine($"create_subscription_error=0x{create.Result.ErrorCode:X8} status=0x{create.Result.Status:X8} specific=0x{create.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
AddMonitoredItemsResponse add = client.AddMonitoredItems(subscriptionId, tags, (ulong)subscribeSampleMs, buffered: subscribeBuffered);
Console.WriteLine($"add_monitored_error=0x{add.Result.ErrorCode:X8} status=0x{add.Result.Status:X8} specific=0x{add.Result.SpecificErrorCode:X8}");
PrintStatuses("add_monitored_status", add.Status);
itemNamesById = AsbPublishMapper.CreateItemNameMap(register.Status, add.Status);
ItemIdentity[] monitoredItems = add.Status?.Select(status => status.Item).ToArray() ?? [];
for (int i = 0; i < publishCount; i++)
{
if (i > 0 && publishDelayMs > 0)
{
Thread.Sleep(TimeSpan.FromMilliseconds(publishDelayMs));
}
AsbPublishResult mapped = client.PublishValues(subscriptionId);
PublishResponse publish = mapped.Response;
Console.WriteLine($"publish[{i}]_error=0x{publish.Result.ErrorCode:X8} status=0x{publish.Result.Status:X8} specific=0x{publish.Result.SpecificErrorCode:X8}");
PrintStatuses($"publish[{i}]_status", publish.Status);
PrintMonitoredValues($"publish[{i}]_value", publish.Values);
PrintPublishedValues($"publish[{i}]_mapped", mapped.Values);
}
if (monitoredItems.Length > 0)
{
DeleteMonitoredItemsResponse deleteItems = client.DeleteMonitoredItems(subscriptionId, monitoredItems);
Console.WriteLine($"delete_monitored_error=0x{deleteItems.Result.ErrorCode:X8} status=0x{deleteItems.Result.Status:X8} specific=0x{deleteItems.Result.SpecificErrorCode:X8}");
PrintStatuses("delete_monitored_status", deleteItems.Status);
}
}
finally
{
if (subscriptionId != 0)
{
DeleteSubscriptionResponse delete = client.DeleteSubscription(subscriptionId);
Console.WriteLine($"delete_subscription_error=0x{delete.Result.ErrorCode:X8} status=0x{delete.Result.Status:X8} specific=0x{delete.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
}
}
}
if (writeVariant.HasValue)
{
const uint writeHandle = 0xA5B21001;
WriteResponse write = client.Write(tag, writeVariant.Value, writeHandle, "MxAsbClient probe write");
Console.WriteLine($"write_error=0x{write.Result.ErrorCode:X8} status=0x{write.Result.Status:X8} specific=0x{write.Result.SpecificErrorCode:X8} handle=0x{writeHandle:X8}");
PrintStatuses("write_status", write.Status);
if (waitWriteComplete)
{
AsbWriteCompletionOptions options = new()
{
Timeout = TimeSpan.FromMilliseconds(writeCompleteTimeoutMs),
PollInterval = TimeSpan.FromMilliseconds(writeCompletePollMs),
ReadbackDelay = TimeSpan.FromMilliseconds(writeReadbackDelayMs),
};
AsbWriteCompletionReadbackResult completionAndReadback = client.WaitForWriteCompleteAndRead(tag, writeHandle, options);
AsbWriteCompletionResult completion = completionAndReadback.Completion;
Console.WriteLine($"write_completion handle=0x{completion.WriteHandle:X8} completed={completion.Completed} timed_out={completion.TimedOut} elapsed_ms={(long)completion.Elapsed.TotalMilliseconds} polls={completion.PollCount} raw_count={completion.CompleteWrites.Count}");
for (int i = 0; i < completion.Responses.Count; i++)
{
PublishWriteCompleteResponse response = completion.Responses[i];
Console.WriteLine($"write_completion_poll[{i}]_error=0x{response.Result.ErrorCode:X8} status=0x{response.Result.Status:X8} specific=0x{response.Result.SpecificErrorCode:X8} count={response.CompleteWrites?.Length ?? 0}");
}
PrintWriteCompletes("write_completion_raw", completion.CompleteWrites);
if (completion.MatchingComplete.HasValue)
{
PrintWriteCompletes("write_completion_match", [completion.MatchingComplete.Value]);
}
if (writeReadbackDelayMs > 0 && completion.Completed)
{
Console.WriteLine($"read_after_write_delay_ms={writeReadbackDelayMs}");
}
if (completionAndReadback.Readback is not null)
{
read = completionAndReadback.Readback;
Console.WriteLine($"read_after_write_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("read_after_write_status", read.Status);
PrintValues("read_after_write_value", read.Values);
}
}
else
{
PublishWriteCompleteResponse complete = client.PublishWriteComplete();
Console.WriteLine($"publish_write_complete_error=0x{complete.Result.ErrorCode:X8} status=0x{complete.Result.Status:X8} specific=0x{complete.Result.SpecificErrorCode:X8}");
Console.WriteLine($"publish_write_complete_count={complete.CompleteWrites?.Length ?? 0}");
PrintWriteCompletes("publish_write_complete", complete.CompleteWrites ?? []);
if (writeReadbackDelayMs > 0)
{
Console.WriteLine($"read_after_write_delay_ms={writeReadbackDelayMs}");
Thread.Sleep(TimeSpan.FromMilliseconds(writeReadbackDelayMs));
}
read = client.Read(tag);
Console.WriteLine($"read_after_write_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("read_after_write_status", read.Status);
PrintValues("read_after_write_value", read.Values);
}
}
UnregisterItemsResponse unregister = registeredItems.Length > 0
? client.UnregisterMany(registeredItems)
: client.Unregister(tag);
Console.WriteLine($"unregister_error=0x{unregister.Result.ErrorCode:X8} status=0x{unregister.Result.Status:X8} specific=0x{unregister.Result.SpecificErrorCode:X8}");
PrintStatuses("unregister_status", unregister.Status);
static void PrintStatuses(string prefix, ItemStatus[]? statuses)
{
if (statuses is null)
{
return;
}
for (int i = 0; i < statuses.Length; i++)
{
ItemStatus status = statuses[i];
AsbItemStatusSummary summary = AsbResultMapper.ToItemSummary(status);
Console.WriteLine($"{prefix}[{i}]=item:{status.Item.Name} id:{status.Item.Id} id_specified:{status.Item.IdSpecified} error:0x{status.ErrorCode:X8} error_name:{summary.Error} error_specified:{status.ErrorCodeSpecified} status_count:{status.Status.Count} status_payload_len:{status.Status.Payload?.Length ?? 0} status:{FormatStatusElements(summary.Status)}");
}
}
static void PrintValues(string prefix, RuntimeValue[]? values)
{
if (values is null)
{
return;
}
for (int i = 0; i < values.Length; i++)
{
RuntimeValue value = values[i];
Console.WriteLine($"{prefix}[{i}]=type:{value.Value.Type} length:{value.Value.Length} payload_len:{value.Value.Payload?.Length ?? 0} preview:{MxAsbDataClient.FormatVariant(value.Value)}");
Console.WriteLine($"{prefix}[{i}].timestamp={value.Timestamp:o} timestamp_specified={value.TimestampSpecified}");
Console.WriteLine($"{prefix}[{i}].status_count={value.Status.Count} status_payload_len={value.Status.Payload?.Length ?? 0} status:{FormatStatusElements(AsbPublishMapper.DecodeStatus(value.Status))}");
}
}
static void PrintMonitoredValues(string prefix, MonitoredItemValue[]? values)
{
if (values is null)
{
return;
}
for (int i = 0; i < values.Length; i++)
{
MonitoredItemValue item = values[i];
RuntimeValue value = item.Value;
Console.WriteLine($"{prefix}[{i}]=item:{item.Item.Name} id:{item.Item.Id} id_specified:{item.Item.IdSpecified} type:{value.Value.Type} length:{value.Value.Length} payload_len:{value.Value.Payload?.Length ?? 0} preview:{MxAsbDataClient.FormatVariant(value.Value)}");
Console.WriteLine($"{prefix}[{i}].timestamp={value.Timestamp:o} timestamp_specified={value.TimestampSpecified}");
Console.WriteLine($"{prefix}[{i}].status_count={value.Status.Count} status_payload_len={value.Status.Payload?.Length ?? 0}");
Console.WriteLine($"{prefix}[{i}].userdata_type={item.UserData.Type} userdata_length={item.UserData.Length} userdata_payload_len={item.UserData.Payload?.Length ?? 0}");
}
}
static void PrintPublishedValues(string prefix, IReadOnlyList<AsbPublishedValue> values)
{
for (int i = 0; i < values.Count; i++)
{
AsbPublishedValue value = values[i];
Console.WriteLine($"{prefix}[{i}]=item:{value.ItemName ?? string.Empty} id:{value.ItemId} type:{value.VariantType} quality:{FormatNullableHex(value.Quality)} timestamp:{value.TimestampUtc:O} preview:{value.Preview}");
Console.WriteLine($"{prefix}[{i}].status={FormatStatusElements(value.Status)} raw_count:{value.RawStatus.Count} raw_payload_len:{value.RawStatus.Payload?.Length ?? 0}");
}
}
static void PrintWriteCompletes(string prefix, IReadOnlyList<ItemWriteComplete> writes)
{
for (int i = 0; i < writes.Count; i++)
{
ItemWriteComplete item = writes[i];
Console.WriteLine($"{prefix}[{i}]=handle:{item.WriteHandle} handle_hex:0x{item.WriteHandle:X8} handle_specified:{item.WriteHandleSpecified} status_items:{item.Status?.Length ?? 0}");
PrintStatuses($"{prefix}[{i}].status", item.Status);
}
}
static void PrintPublishWriteComplete(string prefix, PublishWriteCompleteResponse response)
{
Console.WriteLine($"{prefix}_write_complete_error=0x{response.Result.ErrorCode:X8} status=0x{response.Result.Status:X8} specific=0x{response.Result.SpecificErrorCode:X8} count={response.CompleteWrites?.Length ?? 0}");
PrintWriteCompletes($"{prefix}_write_complete", response.CompleteWrites ?? []);
}
static string FormatStatusElements(IReadOnlyList<AsbStatusElement> status)
{
return status.Count == 0
? string.Empty
: string.Join("|", status.Select(item => $"{item.Type}:{item.Value}"));
}
static string FormatNullableHex(ushort? value)
{
return value.HasValue ? $"0x{value.Value:X4}" : string.Empty;
}
static void RunInvalidTargetProbe(MxAsbDataClient client, string invalidTag, int writeCompleteTimeoutMs, int writeCompletePollMs)
{
Console.WriteLine($"probe_invalid_targets tag={invalidTag}");
RegisterItemsResponse register = client.RegisterMany([invalidTag]);
Console.WriteLine($"invalid_register_error=0x{register.Result.ErrorCode:X8} status=0x{register.Result.Status:X8} specific=0x{register.Result.SpecificErrorCode:X8}");
PrintStatuses("invalid_register_status", register.Status);
ReadResponse read = client.ReadMany([invalidTag]);
Console.WriteLine($"invalid_read_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("invalid_read_status", read.Status);
PrintValues("invalid_read_value", read.Values);
const uint writeHandle = 0xA5B2E001;
WriteResponse write = client.Write(invalidTag, AsbVariantFactory.FromInt32(1), writeHandle, "MxAsbClient probe invalid-target write");
Console.WriteLine($"invalid_write_error=0x{write.Result.ErrorCode:X8} status=0x{write.Result.Status:X8} specific=0x{write.Result.SpecificErrorCode:X8} handle=0x{writeHandle:X8}");
PrintStatuses("invalid_write_status", write.Status);
PrintWriteCompletionProbe(client, "invalid_write_completion", writeHandle, writeCompleteTimeoutMs, writeCompletePollMs);
ItemIdentity[] registeredItems = register.Status?.Select(status => status.Item).ToArray() ?? [];
UnregisterItemsResponse unregister = registeredItems.Length > 0
? client.UnregisterMany(registeredItems)
: client.Unregister(invalidTag);
Console.WriteLine($"invalid_unregister_error=0x{unregister.Result.ErrorCode:X8} status=0x{unregister.Result.Status:X8} specific=0x{unregister.Result.SpecificErrorCode:X8}");
PrintStatuses("invalid_unregister_status", unregister.Status);
}
static void RunWrongTypeWriteProbe(MxAsbDataClient client, string tag, int writeCompleteTimeoutMs, int writeCompletePollMs)
{
Console.WriteLine($"probe_wrong_type_write tag={tag}");
const uint writeHandle = 0xA5B2E002;
WriteResponse write = client.Write(tag, AsbVariantFactory.FromString("wrong-type-write-probe"), writeHandle, "MxAsbClient probe wrong-type write");
Console.WriteLine($"wrong_type_write_error=0x{write.Result.ErrorCode:X8} status=0x{write.Result.Status:X8} specific=0x{write.Result.SpecificErrorCode:X8} handle=0x{writeHandle:X8}");
PrintStatuses("wrong_type_write_status", write.Status);
PrintWriteCompletionProbe(client, "wrong_type_write_completion", writeHandle, writeCompleteTimeoutMs, writeCompletePollMs);
ReadResponse read = client.Read(tag);
Console.WriteLine($"wrong_type_read_after_error=0x{read.Result.ErrorCode:X8} status=0x{read.Result.Status:X8} specific=0x{read.Result.SpecificErrorCode:X8}");
PrintStatuses("wrong_type_read_after_status", read.Status);
PrintValues("wrong_type_read_after_value", read.Values);
}
static void PrintWriteCompletionProbe(
MxAsbDataClient client,
string prefix,
uint writeHandle,
int writeCompleteTimeoutMs,
int writeCompletePollMs)
{
AsbWriteCompletionOptions options = new()
{
Timeout = TimeSpan.FromMilliseconds(writeCompleteTimeoutMs),
PollInterval = TimeSpan.FromMilliseconds(writeCompletePollMs),
};
AsbWriteCompletionResult completion = client.WaitForWriteComplete(writeHandle, options);
Console.WriteLine($"{prefix} handle=0x{completion.WriteHandle:X8} completed={completion.Completed} timed_out={completion.TimedOut} elapsed_ms={(long)completion.Elapsed.TotalMilliseconds} polls={completion.PollCount} raw_count={completion.CompleteWrites.Count}");
for (int i = 0; i < completion.Responses.Count; i++)
{
PublishWriteCompleteResponse response = completion.Responses[i];
Console.WriteLine($"{prefix}_poll[{i}]_error=0x{response.Result.ErrorCode:X8} status=0x{response.Result.Status:X8} specific=0x{response.Result.SpecificErrorCode:X8} count={response.CompleteWrites?.Length ?? 0}");
}
PrintWriteCompletes($"{prefix}_raw", completion.CompleteWrites);
if (completion.MatchingComplete.HasValue)
{
PrintWriteCompletes($"{prefix}_match", [completion.MatchingComplete.Value]);
}
}
static void RunInvalidCleanupProbe(MxAsbDataClient client, int subscribeSampleMs)
{
Console.WriteLine("probe_invalid_cleanup=True");
long subscriptionId = 0;
try
{
CreateSubscriptionResponse create = client.CreateSubscription(maxQueueSize: 128, sampleInterval: (ulong)subscribeSampleMs);
subscriptionId = create.SubscriptionId;
Console.WriteLine($"invalid_cleanup_create_subscription_error=0x{create.Result.ErrorCode:X8} status=0x{create.Result.Status:X8} specific=0x{create.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
ItemIdentity invalidItem = CreateInvalidItemIdentity();
DeleteMonitoredItemsResponse deleteItems = client.DeleteMonitoredItems(subscriptionId, [invalidItem]);
Console.WriteLine($"invalid_cleanup_delete_monitored_error=0x{deleteItems.Result.ErrorCode:X8} status=0x{deleteItems.Result.Status:X8} specific=0x{deleteItems.Result.SpecificErrorCode:X8}");
PrintStatuses("invalid_cleanup_delete_monitored_status", deleteItems.Status);
long invalidSubscriptionId = subscriptionId == long.MaxValue ? subscriptionId - 1 : subscriptionId + 987654321;
DeleteSubscriptionResponse deleteInvalid = client.DeleteSubscription(invalidSubscriptionId);
Console.WriteLine($"invalid_cleanup_delete_subscription_error=0x{deleteInvalid.Result.ErrorCode:X8} status=0x{deleteInvalid.Result.Status:X8} specific=0x{deleteInvalid.Result.SpecificErrorCode:X8} subscription_id={invalidSubscriptionId}");
}
finally
{
if (subscriptionId != 0)
{
DeleteSubscriptionResponse delete = client.DeleteSubscription(subscriptionId);
Console.WriteLine($"invalid_cleanup_delete_valid_subscription_error=0x{delete.Result.ErrorCode:X8} status=0x{delete.Result.Status:X8} specific=0x{delete.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
}
}
}
static void RunEmptyPublishProbe(MxAsbDataClient client, int publishCount, int subscribeSampleMs, int publishDelayMs)
{
Console.WriteLine("probe_empty_publish=True");
long subscriptionId = 0;
try
{
CreateSubscriptionResponse create = client.CreateSubscription(maxQueueSize: 128, sampleInterval: (ulong)subscribeSampleMs);
subscriptionId = create.SubscriptionId;
Console.WriteLine($"empty_publish_create_subscription_error=0x{create.Result.ErrorCode:X8} status=0x{create.Result.Status:X8} specific=0x{create.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
for (int i = 0; i < publishCount; i++)
{
if (i > 0 && publishDelayMs > 0)
{
Thread.Sleep(TimeSpan.FromMilliseconds(publishDelayMs));
}
AsbPublishResult mapped = client.PublishValues(subscriptionId);
PublishResponse publish = mapped.Response;
Console.WriteLine($"empty_publish[{i}]_error=0x{publish.Result.ErrorCode:X8} status=0x{publish.Result.Status:X8} specific=0x{publish.Result.SpecificErrorCode:X8}");
PrintStatuses($"empty_publish[{i}]_status", publish.Status);
PrintMonitoredValues($"empty_publish[{i}]_value", publish.Values);
PrintPublishedValues($"empty_publish[{i}]_mapped", mapped.Values);
}
}
finally
{
if (subscriptionId != 0)
{
DeleteSubscriptionResponse delete = client.DeleteSubscription(subscriptionId);
Console.WriteLine($"empty_publish_delete_subscription_error=0x{delete.Result.ErrorCode:X8} status=0x{delete.Result.Status:X8} specific=0x{delete.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
}
}
}
static void RunOperationCompleteCandidateProbe(MxAsbDataClient client, string[] tags, int subscribeSampleMs, int publishDelayMs)
{
Console.WriteLine("probe_operation_complete_candidates=True");
PrintPublishWriteComplete("operation_candidate_initial", client.PublishWriteComplete());
long subscriptionId = 0;
try
{
CreateSubscriptionResponse create = client.CreateSubscription(maxQueueSize: 128, sampleInterval: (ulong)subscribeSampleMs);
subscriptionId = create.SubscriptionId;
Console.WriteLine($"operation_candidate_create_subscription_error=0x{create.Result.ErrorCode:X8} status=0x{create.Result.Status:X8} specific=0x{create.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
PrintPublishWriteComplete("operation_candidate_after_create_subscription", client.PublishWriteComplete());
AddMonitoredItemsResponse add = client.AddMonitoredItems(subscriptionId, tags, (ulong)subscribeSampleMs);
Console.WriteLine($"operation_candidate_add_monitored_error=0x{add.Result.ErrorCode:X8} status=0x{add.Result.Status:X8} specific=0x{add.Result.SpecificErrorCode:X8}");
PrintStatuses("operation_candidate_add_monitored_status", add.Status);
PrintPublishWriteComplete("operation_candidate_after_add_monitored", client.PublishWriteComplete());
if (publishDelayMs > 0)
{
Thread.Sleep(TimeSpan.FromMilliseconds(publishDelayMs));
}
PublishResponse publish = client.Publish(subscriptionId);
Console.WriteLine($"operation_candidate_publish_error=0x{publish.Result.ErrorCode:X8} status=0x{publish.Result.Status:X8} specific=0x{publish.Result.SpecificErrorCode:X8}");
PrintStatuses("operation_candidate_publish_status", publish.Status);
PrintMonitoredValues("operation_candidate_publish_value", publish.Values);
PrintPublishWriteComplete("operation_candidate_after_publish", client.PublishWriteComplete());
ItemIdentity[] monitoredItems = add.Status?.Select(status => status.Item).ToArray() ?? [];
if (monitoredItems.Length > 0)
{
DeleteMonitoredItemsResponse deleteItems = client.DeleteMonitoredItems(subscriptionId, monitoredItems);
Console.WriteLine($"operation_candidate_delete_monitored_error=0x{deleteItems.Result.ErrorCode:X8} status=0x{deleteItems.Result.Status:X8} specific=0x{deleteItems.Result.SpecificErrorCode:X8}");
PrintStatuses("operation_candidate_delete_monitored_status", deleteItems.Status);
PrintPublishWriteComplete("operation_candidate_after_delete_monitored", client.PublishWriteComplete());
}
}
finally
{
if (subscriptionId != 0)
{
DeleteSubscriptionResponse delete = client.DeleteSubscription(subscriptionId);
Console.WriteLine($"operation_candidate_delete_subscription_error=0x{delete.Result.ErrorCode:X8} status=0x{delete.Result.Status:X8} specific=0x{delete.Result.SpecificErrorCode:X8} subscription_id={subscriptionId}");
PrintPublishWriteComplete("operation_candidate_after_delete_subscription", client.PublishWriteComplete());
}
}
}
static ItemIdentity CreateInvalidItemIdentity()
{
return new ItemIdentity
{
Type = (ushort)ItemIdentityType.Name,
ReferenceType = (ushort)ItemReferenceType.Absolute,
Name = "DefinitelyMissingObject.DefinitelyMissingAttribute",
ContextName = string.Empty,
Id = ulong.MaxValue,
IdSpecified = true,
};
}
static void RunCompatibilitySubscribe(
string endpoint,
string? solution,
string[] tags,
bool dumpMessages,
int publishCount,
int subscribeSampleMs,
int publishDelayMs)
{
using MxAsbCompatibilityServer server = new();
int dataChangeCount = 0;
server.DataChanged += (_, evt) =>
{
dataChangeCount++;
Console.WriteLine($"compat_data_change[{dataChangeCount - 1}]=server:{evt.ServerHandle} item:{evt.ItemHandle} quality:0x{evt.Quality:X4} timestamp:{evt.TimestampUtc:O} value:{FormatObject(evt.Value)} status:{FormatStatusElements(evt.Status)}");
};
int serverHandle = server.Register(endpoint, solution, Console.WriteLine, dumpMessages);
Console.WriteLine($"compat_register_server={serverHandle}");
AsbSubscriptionOptions subscriptionOptions = new()
{
MaxQueueSize = 128,
SampleInterval = (ulong)subscribeSampleMs,
};
AsbMonitoredItemOptions monitoredItemOptions = new()
{
SampleInterval = (ulong)subscribeSampleMs,
};
int[] itemHandles = tags.Select(tag =>
{
int itemHandle = server.AddItem(serverHandle, tag);
Console.WriteLine($"compat_add_item tag:{tag} item:{itemHandle}");
server.Advise(serverHandle, itemHandle, subscriptionOptions, monitoredItemOptions);
Console.WriteLine($"compat_advise item:{itemHandle}");
return itemHandle;
}).ToArray();
for (int i = 0; i < publishCount; i++)
{
if (i > 0 && publishDelayMs > 0)
{
Thread.Sleep(TimeSpan.FromMilliseconds(publishDelayMs));
}
AsbPublishResult result = server.Poll(serverHandle);
Console.WriteLine($"compat_poll[{i}] error=0x{result.Response.Result.ErrorCode:X8} values:{result.Values.Count}");
}
foreach (int itemHandle in itemHandles)
{
server.RemoveItem(serverHandle, itemHandle);
Console.WriteLine($"compat_remove_item item:{itemHandle}");
}
server.Unregister(serverHandle);
Console.WriteLine($"compat_unregister_server={serverHandle} data_changes={dataChangeCount}");
}
static string FormatObject(object? value)
{
return value switch
{
null => string.Empty,
Array array => string.Join(",", array.Cast<object?>()),
DateTime dateTime => dateTime.ToString("O", CultureInfo.InvariantCulture),
TimeSpan timeSpan => timeSpan.ToString("c", CultureInfo.InvariantCulture),
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
_ => value.ToString() ?? string.Empty,
};
}
static void PrintCleanup(string prefix, AsbClientCleanupResult? cleanup)
{
if (cleanup is null)
{
return;
}
Console.WriteLine($"{prefix}=completed:{cleanup.Completed} succeeded:{cleanup.Succeeded} abort:{cleanup.UsedAbortFallback} requires_new:{cleanup.RequiresNewConnection} disconnect_attempted:{cleanup.DisconnectAttempted} disconnect_sent:{cleanup.DisconnectSent} disconnect_failure:{cleanup.DisconnectFailure ?? string.Empty}");
PrintCommunicationCleanup($"{prefix}.channel", cleanup.Channel);
PrintCommunicationCleanup($"{prefix}.factory", cleanup.Factory);
}
static void PrintCommunicationCleanup(string prefix, CommunicationObjectCleanupResult cleanup)
{
Console.WriteLine($"{prefix}=initial:{cleanup.InitialState} final:{cleanup.FinalState} close_attempted:{cleanup.CloseAttempted} closed:{cleanup.Closed} abort_attempted:{cleanup.AbortAttempted} aborted:{cleanup.Aborted} close_failure:{cleanup.CloseFailure ?? string.Empty} abort_failure:{cleanup.AbortFailure ?? string.Empty}");
}
static string? GetArg(string[] args, string name)
{
string prefix = name + "=";
return args.FirstOrDefault(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))?.Substring(prefix.Length);
}
static string[] GetArgs(string[] args, string name)
{
string prefix = name + "=";
return args
.Where(arg => arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(arg => arg.Substring(prefix.Length))
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToArray();
}
static int? TryGetInt(string[] args, string name)
{
return int.TryParse(GetArg(args, name), out int result) ? result : null;
}
static Variant? GetWriteVariant(string[] args)
{
int? writeInt = TryGetInt(args, "--write-int");
if (writeInt.HasValue)
{
return AsbVariantFactory.FromInt32(writeInt.Value);
}
string? writeBool = GetArg(args, "--write-bool");
if (bool.TryParse(writeBool, out bool boolResult))
{
return AsbVariantFactory.FromBoolean(boolResult);
}
string? writeFloat = GetArg(args, "--write-float");
if (float.TryParse(writeFloat, NumberStyles.Float, CultureInfo.InvariantCulture, out float floatResult))
{
return AsbVariantFactory.FromSingle(floatResult);
}
string? writeDouble = GetArg(args, "--write-double");
if (double.TryParse(writeDouble, NumberStyles.Float, CultureInfo.InvariantCulture, out double doubleResult))
{
return AsbVariantFactory.FromDouble(doubleResult);
}
string? writeString = GetArg(args, "--write-string");
if (writeString is not null)
{
return AsbVariantFactory.FromString(writeString);
}
string? writeDateTime = GetArg(args, "--write-datetime");
if (DateTime.TryParse(
writeDateTime,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out DateTime dateTimeResult))
{
return AsbVariantFactory.FromDateTime(dateTimeResult);
}
if (TryParseArray(args, "--write-int-array", int.TryParse, out int[] intArray))
{
return AsbVariantFactory.FromInt32Array(intArray);
}
if (TryParseArray(args, "--write-bool-array", bool.TryParse, out bool[] boolArray))
{
return AsbVariantFactory.FromBooleanArray(boolArray);
}
if (TryParseFloatArray(args, "--write-float-array", out float[] floatArray))
{
return AsbVariantFactory.FromSingleArray(floatArray);
}
if (TryParseDoubleArray(args, "--write-double-array", out double[] doubleArray))
{
return AsbVariantFactory.FromDoubleArray(doubleArray);
}
string? writeStringArray = GetArg(args, "--write-string-array");
if (writeStringArray is not null)
{
return AsbVariantFactory.FromStringArray(writeStringArray.Split('|'));
}
if (TryParseDateTimeArray(args, "--write-datetime-array", out DateTime[] dateTimeArray))
{
return AsbVariantFactory.FromDateTimeArray(dateTimeArray);
}
return null;
}
static bool TryParseArray<T>(string[] args, string name, TryParse<T> parser, out T[] values)
{
string? raw = GetArg(args, name);
if (raw is null)
{
values = [];
return false;
}
string[] parts = raw.Split(',', StringSplitOptions.TrimEntries);
values = new T[parts.Length];
for (int i = 0; i < parts.Length; i++)
{
if (!parser(parts[i], out values[i]))
{
values = [];
return false;
}
}
return true;
}
static bool TryParseFloatArray(string[] args, string name, out float[] values)
{
return TryParseArray(args, name, TryParseFloat, out values);
}
static bool TryParseDoubleArray(string[] args, string name, out double[] values)
{
return TryParseArray(args, name, TryParseDouble, out values);
}
static bool TryParseDateTimeArray(string[] args, string name, out DateTime[] values)
{
return TryParseArray(args, name, TryParseDateTime, out values);
}
static bool TryParseFloat(string value, out float result)
{
return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result);
}
static bool TryParseDouble(string value, out double result)
{
return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result);
}
static bool TryParseDateTime(string value, out DateTime result)
{
return DateTime.TryParse(
value,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out result);
}
static bool HasArg(string[] args, string name)
{
return args.Any(arg => arg.Equals(name, StringComparison.OrdinalIgnoreCase));
}
static string FormatException(Exception ex)
{
return $"{ex.GetType().Name}:0x{ex.HResult:X8}:{ex.Message}";
}
delegate bool TryParse<T>(string value, out T result);