104efc4e9b
Three wire-level bugs surfaced by side-by-side relay capture against
the .NET probe routed via the new --via flag:
1. **Dynamic-dictionary id drift**. Our `encode_envelope` hardcoded
action_dict_id=1 / to_dict_id=3, which is correct for the FIRST
message in a session but wrong for every subsequent one. The
per-session dynamic dict accumulates across messages: Connect's
binary header pre-pops [action,to] at ids 1,3; AuthenticateMe must
reference the new action at id 5 (continuing the odd sequence) and
the To URL at id 3 (still in the dict from Connect). Fix uses
`DynamicDictionary::position_of` + `intern` to compute the right
wire id, only pre-popping strings that are NEW to the session.
Captured against .NET probe via asb-relay: AuthenticateMe binary
header has only one string (action) at offset 0x260 (`06 de 08 2f
2e ...`), and `<a:Action>` value `ab 05` references the new id 5.
2. **ConnectionValidator wire format depends on operation**. .NET's
`IAsbDataV2` declares `[XmlSerializerFormat]` on AuthenticateMe,
Disconnect, KeepAlive (one-way ops) — those use XmlSerializer for
the ENTIRE message including the [MessageHeader] ConnectionValid-
ator. Other ops use the default DataContractSerializer. The wire
shapes differ:
XmlSerializer: `<ConnectionId xmlns="http://asb.contracts.data/
20111111">guid</ConnectionId>` (PascalCase property name in
data namespace)
DataContract: `<connectionIdField xmlns="http://schemas.data
contract.org/2004/07/ArchestrAServices.ASBContract">guid</…>`
(private "fooField" name in datacontract namespace)
New `ValidatorWireFormat::for_action` picks the right shape per
action; `encode_validator` now branches on it. New helpers
`push_xml_text_field` / `push_xml_byte_array_field` for the
XmlSerializer form. The DataContract form is preserved verbatim
for Register/Read/Write/etc.
3. **Decoder missing 0x0A** (`ShortDictionaryXmlnsAttribute`). The
server's RegisterItemsResponse uses `0x0A {dict-id}` to set the
default namespace from the static dict; our decoder bailed out
with `UnknownRecord(10)`. New decode arm produces a
`DefaultNamespace` token with `DictionaryStatic` value.
**.NET probe gains a `--via` flag** (`AsbConnectionOptions.Via` →
`ChannelFactory.CreateChannel(addr, viaUri)`) so the probe can be
routed through asb-relay for byte-level capture without triggering
an `AddressFilterMismatch` fault. CoreWCF / .NET 10 dropped
`ClientViaBehavior`; the `CreateChannel(addr, via)` overload is the
modern equivalent.
Live status (this commit): Connect handshake works, AuthenticateMe
no longer faults (canonical XML + crypto + wire-format all match
.NET now), RegisterItemsResponse comes back from the server (a real
response, not a dispatcher fault). One remaining issue: our response
decoder hits `MissingField { field: "Status" }` — the server's
RegisterItemsResponse uses a slightly different element naming or
encoding than `collect_asbidata_payloads` expects. Next iteration
hunts that.
Workspace: 710 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1093 lines
48 KiB
C#
1093 lines
48 KiB
C#
using MxAsbClient;
|
|
using System.Globalization;
|
|
|
|
string endpoint = GetArg(args, "--endpoint")
|
|
?? "net.tcp://desktop-6jl3kko/ASBService/Default_ZB_MxDataProvider/IDataV2";
|
|
// `--via` overrides the TCP destination without changing the `<a:To>`
|
|
// SOAP header (so the registered service URL still matches inside
|
|
// SMSvcHost). Use to route the probe through `asb-relay` for wire
|
|
// byte capture, e.g. `--via net.tcp://127.0.0.1:8088/...`.
|
|
string? clientVia = GetArg(args, "--via");
|
|
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;
|
|
}
|
|
|
|
// `--dump-signed-xml` produces deterministic .NET `XmlSerializer` output
|
|
// for each ConnectedRequest type that goes through `AsbSystemAuthenticator
|
|
// .Sign` (`AsbSystemAuthenticator.cs:79`). The output is exactly what
|
|
// the .NET HMAC computation runs over, so the Rust port's canonical-XML
|
|
// emitter (F28) needs to produce byte-identical bytes for every type
|
|
// listed here. Connection IDs, MACs, IVs, and message numbers are pinned
|
|
// to deterministic values so the dump is reproducible.
|
|
if (args.Any(arg => arg.Equals("--dump-signed-xml", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
Guid connectionId = Guid.Parse("8cba964a-74c1-ef74-f6aa-761b3540191b");
|
|
byte[] mac = Convert.FromBase64String("AAECAwQFBgcICQoLDA0ODw==");
|
|
byte[] sigIv = Convert.FromBase64String("EBESExQVFhcYGRobHB0eHw==");
|
|
|
|
void Dump(string label, object request)
|
|
{
|
|
string xml = AsbSerialization.ToXml(request);
|
|
byte[] xmlBytes = System.Text.Encoding.UTF8.GetBytes(xml);
|
|
Console.WriteLine($"--- {label} ({xmlBytes.Length} UTF-8 bytes) ---");
|
|
Console.WriteLine(xml);
|
|
Console.WriteLine($"--- {label} (base64) ---");
|
|
Console.WriteLine(Convert.ToBase64String(xmlBytes));
|
|
}
|
|
|
|
ConnectionValidator validator = new()
|
|
{
|
|
ConnectionId = connectionId,
|
|
MessageNumber = 42,
|
|
MessageAuthenticationCode = mac,
|
|
SignatureInitializationVector = sigIv,
|
|
};
|
|
|
|
// The actual signing flow uses an EMPTY MessageAuthenticationCode +
|
|
// SignatureInitializationVector at the time of HMAC computation
|
|
// (`AsbSystemAuthenticator.Sign:79` calls request.ToXml() while the
|
|
// validator's MAC/IV are still `[]`; the encrypt-and-fill happens
|
|
// immediately after). The Rust port has to know what XmlSerializer
|
|
// emits for `byte[] = []` to produce HMAC-matching XML — capture
|
|
// the variant with empty MAC + IV so we can pin both shapes.
|
|
ConnectionValidator emptyValidator = new()
|
|
{
|
|
ConnectionId = connectionId,
|
|
MessageNumber = 42,
|
|
MessageAuthenticationCode = [],
|
|
SignatureInitializationVector = [],
|
|
};
|
|
|
|
AuthenticateMe authMeEmpty = new()
|
|
{
|
|
ConnectionValidator = emptyValidator,
|
|
ConsumerAuthenticationData = new AuthenticationData
|
|
{
|
|
Data = Convert.FromBase64String("ZGV0ZXJtaW5pc3RpYy1jaXBoZXJ0ZXh0LWJ5dGVz"),
|
|
InitializationVector = Convert.FromBase64String("MDEyMzQ1Njc4OWFiY2RlZg=="),
|
|
},
|
|
};
|
|
Dump("AuthenticateMe-empty-mac-iv", authMeEmpty);
|
|
|
|
AuthenticateMe authMe = new()
|
|
{
|
|
ConnectionValidator = validator,
|
|
ConsumerAuthenticationData = new AuthenticationData
|
|
{
|
|
Data = Convert.FromBase64String("ZGV0ZXJtaW5pc3RpYy1jaXBoZXJ0ZXh0LWJ5dGVz"),
|
|
InitializationVector = Convert.FromBase64String("MDEyMzQ1Njc4OWFiY2RlZg=="),
|
|
},
|
|
};
|
|
Dump("AuthenticateMe", authMe);
|
|
|
|
Disconnect disconnect = new()
|
|
{
|
|
ConnectionValidator = validator,
|
|
ConsumerAuthenticationData = new AuthenticationData
|
|
{
|
|
Data = Convert.FromBase64String("ZGlzY29ubmVjdC1jaXBoZXJ0ZXh0"),
|
|
InitializationVector = Convert.FromBase64String("MDEyMzQ1Njc4OWFiY2RlZg=="),
|
|
},
|
|
};
|
|
Dump("Disconnect", disconnect);
|
|
|
|
KeepAlive keepAlive = new() { ConnectionValidator = validator };
|
|
Dump("KeepAlive", keepAlive);
|
|
|
|
RegisterItemsRequest registerDump = new()
|
|
{
|
|
ConnectionValidator = validator,
|
|
Items = [new ItemIdentity
|
|
{
|
|
Type = (ushort)ItemIdentityType.Name,
|
|
ReferenceType = (ushort)ItemReferenceType.Absolute,
|
|
Name = "TestChildObject.TestInt",
|
|
ContextName = string.Empty,
|
|
}],
|
|
RequireId = true,
|
|
RegisterOnly = false,
|
|
};
|
|
Dump("RegisterItemsRequest", registerDump);
|
|
|
|
UnregisterItemsRequest unregisterDump = new()
|
|
{
|
|
ConnectionValidator = validator,
|
|
Items = [new ItemIdentity
|
|
{
|
|
Type = (ushort)ItemIdentityType.Id,
|
|
ReferenceType = (ushort)ItemReferenceType.Absolute,
|
|
Id = 0xCAFE_BABE_DEAD_BEEFul,
|
|
IdSpecified = true,
|
|
}],
|
|
};
|
|
Dump("UnregisterItemsRequest", unregisterDump);
|
|
|
|
return;
|
|
}
|
|
|
|
// `--dump-deterministic-hmac` runs the AuthenticateMe sign path with
|
|
// FIXED inputs end-to-end (no randomness): pinned passphrase, prime,
|
|
// generator, private-key bytes, remote-pub bytes, connection ID,
|
|
// message number, AES IV, and consumer-data/IV bytes. Output is the
|
|
// resulting crypto_key, AES key, canonical XML, HMAC-SHA1, and
|
|
// AES-CBC-encrypted MAC. The Rust port uses these as a fixture for a
|
|
// byte-equality unit test that localises any HMAC/AES/derivation
|
|
// divergence vs the .NET reference without depending on session
|
|
// randomness. Mirrors the per-step decomposition of `AsbSystemAuthent
|
|
// icator.Sign` (`AsbSystemAuthenticator.cs:62-82`) but inlines the
|
|
// math so we control every byte of input.
|
|
if (args.Any(arg => arg.Equals("--dump-deterministic-hmac", StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
System.Numerics.BigInteger prime = System.Numerics.BigInteger.Parse(AsbSolutionCryptoParameters.DefaultPrimeText);
|
|
System.Numerics.BigInteger generator = 22;
|
|
|
|
// 33 bytes: 0x01..0x20 with trailing 0x00 sign byte. Mirrors the
|
|
// shape `AsbSystemAuthenticator.CreatePrivateKey` produces.
|
|
byte[] privateKeyBytes = new byte[33];
|
|
for (int i = 0; i < 32; i++) { privateKeyBytes[i] = (byte)(i + 1); }
|
|
privateKeyBytes[32] = 0x00;
|
|
|
|
// Remote public key — 128 bytes (1024-bit), high bit clear so
|
|
// .NET's BigInteger LE-two's-complement reads it as positive
|
|
// without a sign-byte fix-up.
|
|
byte[] remotePub = new byte[128];
|
|
for (int i = 0; i < 127; i++) { remotePub[i] = (byte)((i * 7 + 13) & 0xFF); }
|
|
remotePub[127] = 0x7F;
|
|
|
|
string passphrase = "deterministic-hmac-fixture-passphrase-rust-vs-dotnet";
|
|
Guid connectionId = Guid.Parse("8cba964a-74c1-ef74-f6aa-761b3540191b");
|
|
ulong messageNumber = 42;
|
|
|
|
// ConsumerAuthenticationData payload. Encrypted bytes are opaque
|
|
// to the HMAC test (they get base64-embedded in the XML and
|
|
// signed); use deterministic bytes 0x80..0xFF + 0x00..0x4F (208
|
|
// bytes — same as a real AuthenticateMe under a 768-bit prime).
|
|
byte[] consumerData = new byte[208];
|
|
for (int i = 0; i < 208; i++) { consumerData[i] = (byte)((i * 3 + 7) & 0xFF); }
|
|
byte[] consumerIv = new byte[16];
|
|
for (int i = 0; i < 16; i++) { consumerIv[i] = (byte)((i * 11 + 5) & 0xFF); }
|
|
|
|
// Deterministic AES IV for encrypting the HMAC. We pick all-zeros
|
|
// so the Rust test can reproduce without a random-IV injection
|
|
// hack. (The real wire path uses a random IV per call; here we
|
|
// bypass that to make the test reproducible.)
|
|
byte[] aesIv = new byte[16];
|
|
|
|
// ---- crypto_key = shared_secret || passphrase_utf8 ----------
|
|
System.Numerics.BigInteger sharedValue = System.Numerics.BigInteger.ModPow(
|
|
new System.Numerics.BigInteger(remotePub),
|
|
new System.Numerics.BigInteger(privateKeyBytes),
|
|
prime);
|
|
byte[] shared = sharedValue.ToByteArray();
|
|
byte[] cryptoKey = [.. shared, .. System.Text.Encoding.UTF8.GetBytes(passphrase)];
|
|
|
|
// ---- canonical XML (empty MAC + IV) -------------------------
|
|
AuthenticateMe req = new()
|
|
{
|
|
ConnectionValidator = new()
|
|
{
|
|
ConnectionId = connectionId,
|
|
MessageNumber = messageNumber,
|
|
MessageAuthenticationCode = [],
|
|
SignatureInitializationVector = [],
|
|
},
|
|
ConsumerAuthenticationData = new AuthenticationData
|
|
{
|
|
Data = consumerData,
|
|
InitializationVector = consumerIv,
|
|
},
|
|
};
|
|
string xmlText = req.ToXml();
|
|
byte[] xmlBytes = System.Text.Encoding.UTF8.GetBytes(xmlText);
|
|
|
|
// ---- HMAC-SHA1(crypto_key, xml_utf8) ------------------------
|
|
using System.Security.Cryptography.HMACSHA1 hmac = new(cryptoKey);
|
|
byte[] hash = hmac.ComputeHash(xmlBytes);
|
|
|
|
// ---- AES key = PBKDF2-SHA1(base64(crypto_key), salt, 1000) --
|
|
byte[] salt = System.Text.Encoding.ASCII.GetBytes("ArchestrAService");
|
|
byte[] aesKey = System.Security.Cryptography.Rfc2898DeriveBytes.Pbkdf2(
|
|
Convert.ToBase64String(cryptoKey),
|
|
salt,
|
|
iterations: 1000,
|
|
System.Security.Cryptography.HashAlgorithmName.SHA1,
|
|
outputLength: 16);
|
|
|
|
// ---- AES-CBC encrypt(hash) with fixed IV --------------------
|
|
byte[] encryptedMac;
|
|
using (System.Security.Cryptography.Aes aes = System.Security.Cryptography.Aes.Create())
|
|
{
|
|
aes.Key = aesKey;
|
|
aes.IV = aesIv;
|
|
// CBC mode, PKCS7 padding (defaults).
|
|
using System.IO.MemoryStream ms = new();
|
|
using (System.Security.Cryptography.CryptoStream cs = new(
|
|
ms,
|
|
aes.CreateEncryptor(),
|
|
System.Security.Cryptography.CryptoStreamMode.Write))
|
|
{
|
|
cs.Write(hash, 0, hash.Length);
|
|
}
|
|
encryptedMac = ms.ToArray();
|
|
}
|
|
|
|
Console.WriteLine("# deterministic-hmac fixture (.NET reference output)");
|
|
Console.WriteLine($"prime_decimal={prime}");
|
|
Console.WriteLine($"generator={generator}");
|
|
Console.WriteLine($"private_key_hex={Convert.ToHexString(privateKeyBytes)}");
|
|
Console.WriteLine($"remote_pub_hex={Convert.ToHexString(remotePub)}");
|
|
Console.WriteLine($"passphrase={passphrase}");
|
|
Console.WriteLine($"connection_id={connectionId:D}");
|
|
Console.WriteLine($"message_number={messageNumber}");
|
|
Console.WriteLine($"consumer_data_hex={Convert.ToHexString(consumerData)}");
|
|
Console.WriteLine($"consumer_iv_hex={Convert.ToHexString(consumerIv)}");
|
|
Console.WriteLine($"aes_iv_hex={Convert.ToHexString(aesIv)}");
|
|
Console.WriteLine($"shared_secret_hex={Convert.ToHexString(shared)}");
|
|
Console.WriteLine($"shared_secret_len={shared.Length}");
|
|
Console.WriteLine($"crypto_key_hex={Convert.ToHexString(cryptoKey)}");
|
|
Console.WriteLine($"crypto_key_len={cryptoKey.Length}");
|
|
Console.WriteLine($"xml_utf8_len={xmlBytes.Length}");
|
|
Console.WriteLine($"xml_utf8_b64={Convert.ToBase64String(xmlBytes)}");
|
|
Console.WriteLine($"hmac_sha1_hex={Convert.ToHexString(hash)}");
|
|
Console.WriteLine($"aes_key_hex={Convert.ToHexString(aesKey)}");
|
|
Console.WriteLine($"encrypted_mac_hex={Convert.ToHexString(encryptedMac)}");
|
|
Console.WriteLine($"encrypted_mac_len={encryptedMac.Length}");
|
|
return;
|
|
}
|
|
|
|
if (probeConnectFailure)
|
|
{
|
|
try
|
|
{
|
|
using MxAsbDataClient connectFailureClient = MxAsbDataClient.Connect(new AsbConnectionOptions
|
|
{
|
|
Endpoint = endpoint,
|
|
SolutionName = solution,
|
|
Trace = Console.WriteLine,
|
|
DumpMessages = dumpMessages,
|
|
Via = clientVia,
|
|
});
|
|
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(new AsbConnectionOptions
|
|
{
|
|
Endpoint = endpoint,
|
|
SolutionName = solution,
|
|
Trace = Console.WriteLine,
|
|
DumpMessages = dumpMessages,
|
|
Via = clientVia,
|
|
});
|
|
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);
|