Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
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:
@@ -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>
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user