Both devices now serve \.\pipe\vrio and \.\pipe\vplasma for the whole app lifetime alongside the COM rows — the com0com-free path. Framing per the pinned contract (0x00 len data / 0x01 DTR+RTS lines, one lines frame on connect, unknown type = log + drop): PipeFraming/PipeFrameDecoder and VRioPipeService (TX paced at the wire rate, peer DTR edges feed HostHandshake) in VRio.Core, listener-only VPlasmaPipeService twin in VPlasma.Core. Busy pipe names retry, so vRIO's built-in glass and the standalone vPLASMA coexist. README documents the transport and the fork conf lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
135 lines
4.8 KiB
C#
135 lines
4.8 KiB
C#
namespace VRio.Core.Device;
|
|
|
|
/// <summary>
|
|
/// The framing shared with the DOSBox-X fork's <c>namedpipe</c> serial
|
|
/// backend (its <c>serialnamedpipe.h</c> 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:
|
|
///
|
|
/// <code>
|
|
/// 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
|
|
/// </code>
|
|
///
|
|
/// 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.
|
|
/// </summary>
|
|
public static class PipeFraming
|
|
{
|
|
public const byte DataType = 0x00;
|
|
public const byte LinesType = 0x01;
|
|
|
|
/// <summary>Lines-frame bit: the sender's DTR output.</summary>
|
|
public const byte LineDtr = 0x01;
|
|
|
|
/// <summary>Lines-frame bit: the sender's RTS output.</summary>
|
|
public const byte LineRts = 0x02;
|
|
|
|
/// <summary>Both outputs asserted — what a present, powered device reports.</summary>
|
|
public const byte LinesPresent = LineDtr | LineRts;
|
|
|
|
/// <summary>Build a lines frame carrying <paramref name="lines"/>.</summary>
|
|
public static byte[] EncodeLines(byte lines) => new[] { LinesType, lines };
|
|
}
|
|
|
|
/// <summary>
|
|
/// Incremental decoder for <see cref="PipeFraming"/>: feed it raw pipe reads,
|
|
/// get one <see cref="Data"/> event per complete data frame and one
|
|
/// <see cref="Lines"/> 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: <see cref="Feed"/> returns false and
|
|
/// <see cref="Violation"/> says why — drop the connection and
|
|
/// <see cref="Reset"/> before serving the next one.
|
|
/// </summary>
|
|
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;
|
|
|
|
/// <summary>
|
|
/// A complete data frame's payload as (buffer, count). The buffer is
|
|
/// reused across frames — consume it synchronously.
|
|
/// </summary>
|
|
public event Action<byte[], int>? Data;
|
|
|
|
/// <summary>A lines frame's bits (see <see cref="PipeFraming.LineDtr"/>).</summary>
|
|
public event Action<byte>? Lines;
|
|
|
|
/// <summary>Why the stream was rejected, or null while it is healthy.</summary>
|
|
public string? Violation { get; private set; }
|
|
|
|
/// <summary>Forget any partial frame and clear a violation (new connection).</summary>
|
|
public void Reset()
|
|
{
|
|
_state = State.Type;
|
|
_fill = _length = 0;
|
|
Violation = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Consume <paramref name="count"/> received bytes from
|
|
/// <paramref name="buffer"/>. Returns false when the stream violates the
|
|
/// framing contract (see <see cref="Violation"/>); a poisoned decoder
|
|
/// keeps returning false until <see cref="Reset"/>.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|