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;
}
}