namespace VRio.Core.Device; /// /// The framing shared with the DOSBox-X fork's namedpipe serial /// backend (its serialnamedpipe.h header comment is the contract's /// source of truth on that side). A pipe is a plain byte stream, so serial /// data and modem control lines are multiplexed as typed frames: /// /// /// 0x00 <len:u8> <len bytes> serial data, len ≥ 1 (batching allowed) /// 0x01 <lines:u8> the sender's OWN output lines (bit0 DTR, /// bit1 RTS); the receiver applies the /// null-modem cross: peer DTR → local DSR, /// peer RTS → local CTS /// /// /// Each side sends one lines frame immediately on connect; until it arrives /// the peer's lines are assumed low, and a disconnect drops them low again. /// Any other frame type is a protocol bug, not line noise — pipes don't drop /// bytes — so the receiver logs it and drops the connection instead of /// trying to resync. /// public static class PipeFraming { public const byte DataType = 0x00; public const byte LinesType = 0x01; /// Lines-frame bit: the sender's DTR output. public const byte LineDtr = 0x01; /// Lines-frame bit: the sender's RTS output. public const byte LineRts = 0x02; /// Both outputs asserted — what a present, powered device reports. public const byte LinesPresent = LineDtr | LineRts; /// Build a lines frame carrying . public static byte[] EncodeLines(byte lines) => new[] { LinesType, lines }; } /// /// Incremental decoder for : feed it raw pipe reads, /// get one event per complete data frame and one /// event per lines frame. Frames may split across reads /// at any byte boundary. A malformed stream (unknown type, zero-length data /// frame) poisons the decoder: returns false and /// says why — drop the connection and /// before serving the next one. /// public sealed class PipeFrameDecoder { private enum State { Type, Length, Payload, Lines } private readonly byte[] _payload = new byte[byte.MaxValue]; private State _state; private int _fill, _length; /// /// A complete data frame's payload as (buffer, count). The buffer is /// reused across frames — consume it synchronously. /// public event Action? Data; /// A lines frame's bits (see ). public event Action? Lines; /// Why the stream was rejected, or null while it is healthy. public string? Violation { get; private set; } /// Forget any partial frame and clear a violation (new connection). public void Reset() { _state = State.Type; _fill = _length = 0; Violation = null; } /// /// Consume received bytes from /// . Returns false when the stream violates the /// framing contract (see ); a poisoned decoder /// keeps returning false until . /// public bool Feed(byte[] buffer, int count) { if (Violation is not null) return false; for (int i = 0; i < count; i++) { byte b = buffer[i]; switch (_state) { case State.Type when b == PipeFraming.DataType: _state = State.Length; break; case State.Type when b == PipeFraming.LinesType: _state = State.Lines; break; case State.Type: Violation = $"unknown frame type 0x{b:X2}"; return false; case State.Length when b == 0: Violation = "zero-length data frame"; return false; case State.Length: _length = b; _fill = 0; _state = State.Payload; break; case State.Payload: _payload[_fill++] = b; if (_fill == _length) { _state = State.Type; Data?.Invoke(_payload, _length); } break; case State.Lines: _state = State.Type; Lines?.Invoke(b); break; } } return true; } }