namespace ZB.MOM.WW.OtOpcUa.Driver.Ab.Transport.Cip;
/// How CIP requests reach the target's message router (review finding B3; the captures decide
/// each family's mode).
public enum CipRouting
{
/// Forward Open a class-3 connection, then SendUnitData. Logix and Micro800 (empty route).
Connected,
/// SendRRData wrapping each request in Unconnected Send (0x52) carrying the route.
UnconnectedSend,
/// SendRRData carrying the request bare; any configured CIP path is ignored (PCCC families).
Direct,
}
/// Per-device connection settings. Validated on assignment so an unusable value fails at the
/// configuration seam rather than on the wire.
public sealed record ConnectionOptions
{
/// Press safety floor: no setting may let Forward Opens to one device start closer together.
public static readonly TimeSpan MinimumForwardOpenInterval = TimeSpan.FromSeconds(1);
/// Originator serial shared by every connection this process opens.
private static readonly uint ProcessOriginatorSerial = (uint)Random.Shared.Next(1, int.MaxValue);
private readonly TimeSpan _timeout = TimeSpan.FromSeconds(5);
private readonly int _connectionSize = 4002;
private readonly TimeSpan _minForwardOpenInterval = MinimumForwardOpenInterval;
private readonly uint _rpiMicros = 2_000_000;
private readonly byte _timeoutMultiplier = 1;
private readonly TimeSpan? _keepAliveInterval;
/// How requests reach the target's message router.
public CipRouting Routing { get; init; } = CipRouting.Connected;
/// Pack queued requests into Multiple Service Packets (the scheduler's switch).
public bool AllowPacking { get; init; }
/// Bound on each single exchange (TCP connect, RegisterSession, Forward Open, request).
/// Must be finite and positive: an infinite timeout would let an abandoned exchange
/// hold the session's single-flight gate forever.
public TimeSpan Timeout
{
get => _timeout;
init => _timeout = value > TimeSpan.Zero && value != System.Threading.Timeout.InfiniteTimeSpan
? value
: throw new ArgumentOutOfRangeException(nameof(Timeout), value, "Timeout must be finite and positive.");
}
/// Requested connection size in bytes. Above 511 the open is a Large Forward Open (0x5B).
public int ConnectionSize
{
get => _connectionSize;
init => _connectionSize = value is >= 1 and <= ushort.MaxValue
? value
: throw new ArgumentOutOfRangeException(nameof(ConnectionSize), value, "Connection size must be 1..65535.");
}
/// Minimum time between Forward Open attempt starts for one device (≥ 1 s, press safety).
public TimeSpan MinForwardOpenInterval
{
get => _minForwardOpenInterval;
init => _minForwardOpenInterval = value >= MinimumForwardOpenInterval && value != System.Threading.Timeout.InfiniteTimeSpan
? value
: throw new ArgumentOutOfRangeException(nameof(MinForwardOpenInterval), value, "Must be finite and at least 1 s (press safety).");
}
/// Requested packet interval for both directions, in microseconds.
public uint RpiMicros
{
get => _rpiMicros;
init => _rpiMicros = value > 0 ? value : throw new ArgumentOutOfRangeException(nameof(RpiMicros), value, "RPI must be positive.");
}
/// Encoded connection timeout multiplier: the connection expires after RPI × (4 << value).
/// The default 1 means ×8, so 2 s × 8 = 16 s.
public byte TimeoutMultiplier
{
get => _timeoutMultiplier;
init => _timeoutMultiplier = value <= 7 ? value : throw new ArgumentOutOfRangeException(nameof(TimeoutMultiplier), value, "Multiplier must be 0..7.");
}
/// How long an idle connected session waits before the target would drop it.
public TimeSpan ConnectionExpiry => TimeSpan.FromTicks(RpiMicros * 10L * (4L << TimeoutMultiplier));
/// Idle time after which a keep-alive is sent (Connected only). Defaults to half the
/// connection expiry: RPI × 8 / 2 = 8 s.
public TimeSpan KeepAliveInterval
{
get => _keepAliveInterval ?? ConnectionExpiry / 2;
init => _keepAliveInterval = value > TimeSpan.Zero && value != System.Threading.Timeout.InfiniteTimeSpan
? value
: throw new ArgumentOutOfRangeException(nameof(KeepAliveInterval), value, "Keep-alive interval must be finite and positive.");
}
/// The vendor id sent in the Forward Open originator identity.
public ushort VendorId { get; init; } = 0x1337;
/// Random per process; together with the vendor id and connection serial it names a connection.
public uint OriginatorSerial { get; init; } = ProcessOriginatorSerial;
}