plasma: ESC P bitmap rows through the feedback endpoint (plasma row)
PlasmaCommands.GraphicsWrite/GraphicsRow port the display firmware graphics command (ESC P s y x w h, MSB-left); PlasmaDisplay.RowAsync writes a locked whole-row update; the line protocol gains `plasma row <y> <hex32>`. The router plasma slot becomes a bounded FIFO queue: rows stream in order (a frame must not tear), texts still coalesce to the newest, clear flushes, cap 128 with counted drops. Docs updated; 442 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,13 @@ namespace RioJoy.Core.Feedback;
|
||||
/// misconfigured client is diagnosable. <c>lamp-all</c> silently skips
|
||||
/// profile-owned lamps for the same reason.
|
||||
///
|
||||
/// Plasma writes are single-flight with a latest-pending-wins slot, so a
|
||||
/// flooding client cannot queue unbounded 9600-baud text writes.
|
||||
/// Plasma writes are single-flight over a small bounded queue whose rules
|
||||
/// match what each command means: <c>text</c> coalesces (only the newest
|
||||
/// queued text survives — a flooding score updater shows the latest value),
|
||||
/// <c>clear</c> flushes everything queued before it, and bitmap <c>row</c>s
|
||||
/// are FIFO in arrival order (a frame is many rows; coalescing would tear
|
||||
/// it). The bound caps what a flooding client can queue against the
|
||||
/// 9600-baud display port.
|
||||
/// </summary>
|
||||
public sealed class FeedbackRouter
|
||||
{
|
||||
@@ -40,10 +45,13 @@ public sealed class FeedbackRouter
|
||||
public HashSet<int> LoggedOwnedDrops { get; } = new();
|
||||
}
|
||||
|
||||
// ~4 full bitmap frames; beyond this an incoming row is dropped (counted).
|
||||
private const int MaxPlasmaQueue = 128;
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly List<FeedbackCommand> _plasmaQueue = new();
|
||||
private Target? _target;
|
||||
private bool _plasmaBusy;
|
||||
private FeedbackCommand? _plasmaPending;
|
||||
private long _dropped;
|
||||
|
||||
/// <summary>Diagnostics (dropped profile-owned lamp writes, plasma faults).</summary>
|
||||
@@ -63,7 +71,7 @@ public sealed class FeedbackRouter
|
||||
lock (_gate)
|
||||
{
|
||||
_target = new Target(lamps, map, plasma, config);
|
||||
_plasmaPending = null; // pending text belonged to the previous profile
|
||||
_plasmaQueue.Clear(); // queued content belonged to the previous profile
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +81,7 @@ public sealed class FeedbackRouter
|
||||
lock (_gate)
|
||||
{
|
||||
_target = null;
|
||||
_plasmaPending = null;
|
||||
_plasmaQueue.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +110,7 @@ public sealed class FeedbackRouter
|
||||
break;
|
||||
case FeedbackCommandKind.PlasmaText:
|
||||
case FeedbackCommandKind.PlasmaClear:
|
||||
case FeedbackCommandKind.PlasmaRow:
|
||||
DispatchPlasma(target, command);
|
||||
break;
|
||||
}
|
||||
@@ -152,23 +161,66 @@ public sealed class FeedbackRouter
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_plasmaBusy)
|
||||
switch (command.Kind)
|
||||
{
|
||||
if (_plasmaPending is not null)
|
||||
Interlocked.Increment(ref _dropped); // superseded before it ran
|
||||
_plasmaPending = command; // latest wins
|
||||
return;
|
||||
case FeedbackCommandKind.PlasmaClear:
|
||||
// A clear supersedes everything queued before it.
|
||||
for (int i = 0; i < _plasmaQueue.Count; i++)
|
||||
Interlocked.Increment(ref _dropped);
|
||||
_plasmaQueue.Clear();
|
||||
_plasmaQueue.Add(command);
|
||||
break;
|
||||
|
||||
case FeedbackCommandKind.PlasmaText:
|
||||
// Only the newest text survives (a score updater shows the
|
||||
// latest value); queued rows keep their place.
|
||||
for (int i = _plasmaQueue.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_plasmaQueue[i].Kind == FeedbackCommandKind.PlasmaText)
|
||||
{
|
||||
_plasmaQueue.RemoveAt(i);
|
||||
Interlocked.Increment(ref _dropped); // superseded before it ran
|
||||
}
|
||||
}
|
||||
_plasmaQueue.Add(command);
|
||||
break;
|
||||
|
||||
default: // PlasmaRow: strict FIFO — a bitmap frame is many rows
|
||||
if (_plasmaQueue.Count >= MaxPlasmaQueue)
|
||||
{
|
||||
Interlocked.Increment(ref _dropped); // client outran the display
|
||||
return;
|
||||
}
|
||||
_plasmaQueue.Add(command);
|
||||
break;
|
||||
}
|
||||
|
||||
if (_plasmaBusy)
|
||||
return;
|
||||
_plasmaBusy = true;
|
||||
command = TakeQueuedPlasma()!;
|
||||
}
|
||||
StartPlasmaWrite(target, command);
|
||||
}
|
||||
|
||||
// Caller holds _gate.
|
||||
private FeedbackCommand? TakeQueuedPlasma()
|
||||
{
|
||||
if (_plasmaQueue.Count == 0)
|
||||
return null;
|
||||
FeedbackCommand head = _plasmaQueue[0];
|
||||
_plasmaQueue.RemoveAt(0);
|
||||
return head;
|
||||
}
|
||||
|
||||
private void StartPlasmaWrite(Target target, FeedbackCommand command)
|
||||
{
|
||||
Task write = command.Kind == FeedbackCommandKind.PlasmaClear
|
||||
? target.Plasma!.ClearAsync()
|
||||
: target.Plasma!.PosTextAsync(command.Text ?? string.Empty, command.X, command.Y);
|
||||
Task write = command.Kind switch
|
||||
{
|
||||
FeedbackCommandKind.PlasmaClear => target.Plasma!.ClearAsync(),
|
||||
FeedbackCommandKind.PlasmaRow => target.Plasma!.RowAsync(command.Y, command.Data!),
|
||||
_ => target.Plasma!.PosTextAsync(command.Text ?? string.Empty, command.X, command.Y),
|
||||
};
|
||||
|
||||
write.ContinueWith(w =>
|
||||
{
|
||||
@@ -179,18 +231,23 @@ public sealed class FeedbackRouter
|
||||
Target? current;
|
||||
lock (_gate)
|
||||
{
|
||||
next = _plasmaPending;
|
||||
_plasmaPending = null;
|
||||
current = _target; // pending text applies to the *current* profile's display
|
||||
if (next is null || current?.Plasma is null || !current.Config.AllowPlasmaText)
|
||||
current = _target; // queued content applies to the *current* profile's display
|
||||
if (current?.Plasma is null || !current.Config.AllowPlasmaText)
|
||||
{
|
||||
_plasmaBusy = false; // chain ends; a racing Dispatch starts a fresh one
|
||||
if (next is not null)
|
||||
for (int i = 0; i < _plasmaQueue.Count; i++)
|
||||
Interlocked.Increment(ref _dropped);
|
||||
_plasmaQueue.Clear();
|
||||
_plasmaBusy = false;
|
||||
return;
|
||||
}
|
||||
// Busy stays true across the chained write, so latest-wins ordering
|
||||
// holds — concurrent dispatches keep landing in the pending slot.
|
||||
next = TakeQueuedPlasma();
|
||||
if (next is null)
|
||||
{
|
||||
_plasmaBusy = false; // queue drained; a racing Dispatch starts fresh
|
||||
return;
|
||||
}
|
||||
// Busy stays true across the chained write, so queue ordering
|
||||
// holds — concurrent dispatches keep appending behind us.
|
||||
}
|
||||
StartPlasmaWrite(current, next);
|
||||
}, TaskContinuationOptions.ExecuteSynchronously);
|
||||
|
||||
Reference in New Issue
Block a user