feedback: plasma box, explicit fonts, and per-position text coalescing
Three endpoint gaps, found building the Descent 3 score overlay - the boxed place|score field the original games drew over the callsign: plasma box <x> <y> <w> <h> draws an outlined box with a blanked interior, the overlay chrome, as one ESC P graphics write. The wire addresses whole bytes horizontally, so the write covers the byte-aligned span containing the box and clears span pixels outside it; documented, with 8-px alignment the advice. Boxes queue FIFO with rows. plasma text gains an explicit font: a third numeric token after the position, with text still following, so `plasma text 2 2 7` still displays "7". Auto-fit picks the font by LENGTH - short text always rendered large, and a "1" that must fit a 12-px box simply could not be sent before. ResolvePosText generalizes the legacy Score-font special case: 0 = auto, nonzero honored. Text coalescing is now per position. The global rule - any queued text superseded every other queued text - meant the documented two-field layout (callsign top, score bottom) could not survive its own send burst: the second field silently ate the first whenever both were queued. A newer text now replaces only a queued text at the same (x,y). 15 new tests; 472 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+22
-8
@@ -43,11 +43,12 @@ case-insensitive. Lines over 256 bytes and UDP datagrams over 4 KB are dropped.
|
||||
|
||||
```
|
||||
# comment (also ;)
|
||||
lamp <addr> <state> set one lamp
|
||||
lamp-all <state> set every lamp
|
||||
plasma text [x y] <text> write text to the plasma display
|
||||
plasma clear clear the plasma display
|
||||
plasma row <y> <hex32> write one full 128-px bitmap row
|
||||
lamp <addr> <state> set one lamp
|
||||
lamp-all <state> set every lamp
|
||||
plasma text [x y [font]] <text> write text to the plasma display
|
||||
plasma clear clear the plasma display
|
||||
plasma row <y> <hex32> write one full 128-px bitmap row
|
||||
plasma box <x> <y> <w> <h> outlined box with a blanked interior
|
||||
```
|
||||
|
||||
- **`<addr>`** — RIO lamp address, decimal or `0x` hex. Valid: `0x00–0x47`
|
||||
@@ -62,17 +63,30 @@ plasma row <y> <hex32> write one full 128-px bitmap row
|
||||
(`"VIPER 1-1"`; quotes stripped, no escapes). Two leading *numeric* tokens
|
||||
are a cursor position `x y`; omitted (or `0 0`) auto-fits and centers
|
||||
(`PlasmaPosText`). To display something that starts with two numbers, quote
|
||||
it. Encoding is **Latin-1** (one byte = one char, the plasma's wire
|
||||
encoding) — do not send UTF-8 for accented characters.
|
||||
it. A **third** numeric token after the position — with text still following
|
||||
— is an explicit font: `0` auto (by length: ≤9 chars large, else small),
|
||||
`2` small 5×7, `5` large 10×14. Short positioned text otherwise always
|
||||
renders large, which cannot fit inside a `plasma box`. Encoding is
|
||||
**Latin-1** (one byte = one char, the plasma's wire encoding) — do not send
|
||||
UTF-8 for accented characters. Text coalesces **per position**: a newer
|
||||
queued text replaces an older one at the same `x y` only, so multi-field
|
||||
layouts (callsign + score) can update one field without losing the others.
|
||||
- **`plasma row`** — one full bitmap row: `<y>` 0–31 (decimal or `0x` hex),
|
||||
then exactly **32 hex digits** = 16 bytes = 128 pixels, **MSB = leftmost**.
|
||||
Rows are written strictly in arrival order (unlike `plasma text`, which
|
||||
coalesces to the newest — a bitmap frame is many rows and must not tear);
|
||||
coalesces — a bitmap frame is many rows and must not tear);
|
||||
`plasma clear` discards any queued rows/text. Up to 128 commands queue;
|
||||
beyond that incoming rows are dropped and counted — pace full-frame pushes
|
||||
(a 32-row frame is ~0.77 s of wire time at 9600 baud; stream changed rows,
|
||||
as the native games did). See
|
||||
[OUTPUT-INTEGRATION.md](OUTPUT-INTEGRATION.md#bitmap-graphics-esc-p).
|
||||
- **`plasma box`** — an outlined 1-px box with its interior blanked, in pixel
|
||||
coordinates (`x` 0–127, `y` 0–31, must fit the panel). This is the overlay
|
||||
chrome the original games drew for their rank|score field over the callsign
|
||||
bitmap. The wire's graphics command spans whole bytes horizontally, so the
|
||||
write covers the byte-aligned span containing the box; pixels inside the
|
||||
span but outside the box are cleared — place boxes on 8-px boundaries when
|
||||
that matters. Boxes queue FIFO with rows.
|
||||
|
||||
Malformed lines are dropped and counted (first few are logged); they **never**
|
||||
cost a client its connection. The endpoint sends no replies.
|
||||
|
||||
@@ -107,14 +107,22 @@ What the endpoint exposes (v1):
|
||||
position; the font still auto-fits by length. Use this to keep two fields on
|
||||
screen at once (e.g. callsign top line, score bottom line:
|
||||
`plasma text 2 2 "VIPER 1-1"` + `plasma text 2 18 "SCORE 4200"`).
|
||||
- **`plasma text <x> <y> <font> <text>`** — as above with an explicit font
|
||||
(`2` small 5×7, `5` large 10×14, `0` auto). Auto picks the font by LENGTH,
|
||||
so short positioned text always renders large; the explicit form is how a
|
||||
short field ("1", "1000") fits inside a score box.
|
||||
- **`plasma clear`** — blank the display.
|
||||
- **`plasma row <y> <hex32>`** — one full 128-px bitmap row; see
|
||||
[Bitmap graphics](#bitmap-graphics-esc-p).
|
||||
- **`plasma box <x> <y> <w> <h>`** — outlined box, interior blanked: the
|
||||
overlay chrome for a field drawn on top of other content (the original
|
||||
games' rank|score box over the callsign). Byte-aligned horizontally — put
|
||||
box edges on 8-px boundaries where neighbors matter.
|
||||
|
||||
Text is **Latin-1** (one byte per char) — don't send UTF-8.
|
||||
|
||||
Not exposed through the endpoint yet (small extensions when needed): explicit
|
||||
font/attribute selection and box draw/fill.
|
||||
Not exposed through the endpoint yet (small extensions when needed): text
|
||||
attribute selection (intensity/underline/reverse/flash) and filled-only boxes.
|
||||
|
||||
### Bitmap graphics (`ESC P`)
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ public enum FeedbackCommandKind
|
||||
|
||||
/// <summary>One full 128-px bitmap row (<c>plasma row <y> <32 hex digits></c>).</summary>
|
||||
PlasmaRow,
|
||||
|
||||
/// <summary>Outlined box with a blanked interior (<c>plasma box <x> <y> <w> <h></c>).</summary>
|
||||
PlasmaBox,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -47,4 +50,14 @@ public sealed record FeedbackCommand
|
||||
|
||||
/// <summary>Row pixel bytes (<see cref="FeedbackCommandKind.PlasmaRow"/> only; 16 bytes, MSB leftmost).</summary>
|
||||
public byte[]? Data { get; init; }
|
||||
|
||||
/// <summary>Explicit font id for <see cref="FeedbackCommandKind.PlasmaText"/>;
|
||||
/// 0 = auto-fit by length (the default, and the only pre-font behavior).</summary>
|
||||
public byte Font { get; init; }
|
||||
|
||||
/// <summary>Box width in pixels (<see cref="FeedbackCommandKind.PlasmaBox"/> only).</summary>
|
||||
public byte Width { get; init; }
|
||||
|
||||
/// <summary>Box height in pixels (<see cref="FeedbackCommandKind.PlasmaBox"/> only).</summary>
|
||||
public byte Height { get; init; }
|
||||
}
|
||||
|
||||
@@ -166,6 +166,9 @@ public static class FeedbackLineParser
|
||||
case "row":
|
||||
return TryParsePlasmaRow(s, pos, out command, out error);
|
||||
|
||||
case "box":
|
||||
return TryParsePlasmaBox(s, pos, out command, out error);
|
||||
|
||||
default:
|
||||
error = $"unknown plasma subcommand '{sub}'";
|
||||
return false;
|
||||
@@ -231,6 +234,55 @@ public static class FeedbackLineParser
|
||||
return true;
|
||||
}
|
||||
|
||||
// plasma box <x> <y> <w> <h>: outlined box with a blanked interior, pixel
|
||||
// coordinates. The wire's graphics command spans whole bytes, so the write
|
||||
// covers the byte-aligned span containing x..x+w-1; pixels inside that
|
||||
// span but outside the box are cleared (see PlasmaDisplay.BoxAsync).
|
||||
private static bool TryParsePlasmaBox(
|
||||
string s, int pos, out FeedbackCommand? command, out string? error)
|
||||
{
|
||||
command = null;
|
||||
error = null;
|
||||
|
||||
int[] v = new int[4];
|
||||
string[] names = { "x", "y", "w", "h" };
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
string? tok = NextToken(s, ref pos);
|
||||
if (tok is null || !TryParseNumber(tok, out v[i]))
|
||||
{
|
||||
error = "plasma box needs four numbers: x y w h";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (NextToken(s, ref pos) is string extra)
|
||||
{
|
||||
error = $"unexpected token '{extra}'";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (v[0] is < 0 or > 127 || v[1] is < 0 or > 31)
|
||||
{
|
||||
error = $"plasma box position ({v[0]},{v[1]}) out of range (x 0-127, y 0-31)";
|
||||
return false;
|
||||
}
|
||||
if (v[2] < 1 || v[0] + v[2] > 128 || v[3] < 1 || v[1] + v[3] > 32)
|
||||
{
|
||||
error = $"plasma box {v[2]}x{v[3]} at ({v[0]},{v[1]}) exceeds the 128x32 panel";
|
||||
return false;
|
||||
}
|
||||
|
||||
command = new FeedbackCommand
|
||||
{
|
||||
Kind = FeedbackCommandKind.PlasmaBox,
|
||||
X = (byte)v[0],
|
||||
Y = (byte)v[1],
|
||||
Width = (byte)v[2],
|
||||
Height = (byte)v[3],
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int HexNibble(char c) => c switch
|
||||
{
|
||||
>= '0' and <= '9' => c - '0',
|
||||
@@ -247,7 +299,12 @@ public static class FeedbackLineParser
|
||||
|
||||
// Optional "x y" position: taken only when the first TWO tokens are both
|
||||
// numeric (so `plasma text 42` displays "42"; use quotes to force text).
|
||||
byte x = 0, y = 0;
|
||||
// A THIRD numeric token after a position, with text still following, is
|
||||
// an explicit font id (0 = auto-fit by length; 2 small 5x7, 5 large
|
||||
// 10x14) - short positioned text otherwise always renders large, which
|
||||
// cannot fit inside a score box. Unpositioned text takes no font (the
|
||||
// auto-center math chooses it); quote text that starts with numbers.
|
||||
byte x = 0, y = 0, font = 0;
|
||||
int textStart = pos;
|
||||
int peek = pos;
|
||||
string? t1 = NextToken(s, ref peek);
|
||||
@@ -264,6 +321,25 @@ public static class FeedbackLineParser
|
||||
x = (byte)xv;
|
||||
y = (byte)yv;
|
||||
textStart = peek;
|
||||
|
||||
int fontPeek = peek;
|
||||
string? t3 = NextToken(s, ref fontPeek);
|
||||
if (t3 is not null && TryParseNumber(t3, out int fv))
|
||||
{
|
||||
// Only a font when text still follows - `plasma text 2 2 7`
|
||||
// keeps displaying "7" as it always has.
|
||||
if (TryTakeText(s, fontPeek, out string? peekText, out _) &&
|
||||
!string.IsNullOrEmpty(peekText))
|
||||
{
|
||||
if (fv is < 0 or > 7)
|
||||
{
|
||||
error = $"plasma font {fv} out of range (0-7; 0 = auto)";
|
||||
return false;
|
||||
}
|
||||
font = (byte)fv;
|
||||
textStart = fontPeek;
|
||||
}
|
||||
}
|
||||
}
|
||||
// t1 numeric but t2 not: the whole remainder (from textStart) is text
|
||||
}
|
||||
@@ -282,6 +358,7 @@ public static class FeedbackLineParser
|
||||
Text = text,
|
||||
X = x,
|
||||
Y = y,
|
||||
Font = font,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -18,12 +18,13 @@ namespace RioJoy.Core.Feedback;
|
||||
/// profile-owned lamps for the same reason.
|
||||
///
|
||||
/// 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),
|
||||
/// match what each command means: <c>text</c> coalesces per position (only the
|
||||
/// newest queued text at the same (x,y) survives — a flooding score updater
|
||||
/// shows the latest value, while other fields on the glass keep theirs),
|
||||
/// <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.
|
||||
/// and <c>box</c>es 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
|
||||
{
|
||||
@@ -111,6 +112,7 @@ public sealed class FeedbackRouter
|
||||
case FeedbackCommandKind.PlasmaText:
|
||||
case FeedbackCommandKind.PlasmaClear:
|
||||
case FeedbackCommandKind.PlasmaRow:
|
||||
case FeedbackCommandKind.PlasmaBox:
|
||||
DispatchPlasma(target, command);
|
||||
break;
|
||||
}
|
||||
@@ -172,20 +174,30 @@ public sealed class FeedbackRouter
|
||||
break;
|
||||
|
||||
case FeedbackCommandKind.PlasmaText:
|
||||
// Only the newest text survives (a score updater shows the
|
||||
// latest value); queued rows keep their place.
|
||||
// Only the newest text FOR THE SAME POSITION survives (a
|
||||
// score updater shows the latest value). Texts at other
|
||||
// positions are other fields - the documented multi-field
|
||||
// layout (callsign top, score bottom) sends several in a
|
||||
// burst, and the original global coalescing ate all but
|
||||
// the last of them. Queued rows/boxes keep their place.
|
||||
for (int i = _plasmaQueue.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (_plasmaQueue[i].Kind == FeedbackCommandKind.PlasmaText)
|
||||
if (_plasmaQueue[i].Kind == FeedbackCommandKind.PlasmaText &&
|
||||
_plasmaQueue[i].X == command.X && _plasmaQueue[i].Y == command.Y)
|
||||
{
|
||||
_plasmaQueue.RemoveAt(i);
|
||||
Interlocked.Increment(ref _dropped); // superseded before it ran
|
||||
}
|
||||
}
|
||||
if (_plasmaQueue.Count >= MaxPlasmaQueue)
|
||||
{
|
||||
Interlocked.Increment(ref _dropped);
|
||||
return;
|
||||
}
|
||||
_plasmaQueue.Add(command);
|
||||
break;
|
||||
|
||||
default: // PlasmaRow: strict FIFO — a bitmap frame is many rows
|
||||
default: // PlasmaRow/PlasmaBox: strict FIFO — a bitmap frame is many rows
|
||||
if (_plasmaQueue.Count >= MaxPlasmaQueue)
|
||||
{
|
||||
Interlocked.Increment(ref _dropped); // client outran the display
|
||||
@@ -219,7 +231,10 @@ public sealed class FeedbackRouter
|
||||
{
|
||||
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),
|
||||
FeedbackCommandKind.PlasmaBox =>
|
||||
target.Plasma!.BoxAsync(command.X, command.Y, command.Width, command.Height),
|
||||
_ => target.Plasma!.PosTextAsync(command.Text ?? string.Empty, command.X, command.Y,
|
||||
0, command.Font),
|
||||
};
|
||||
|
||||
write.ContinueWith(w =>
|
||||
|
||||
@@ -113,10 +113,13 @@ public static class PlasmaCommands
|
||||
|
||||
/// <summary>
|
||||
/// Compute the auto-fit font and centered (x, y) for positioned text, porting
|
||||
/// the <c>PlasmaPosText</c> layout logic (riovjoy2.cpp#L2235). For non-score
|
||||
/// text (<paramref name="font"/> ≠ 2), the font is chosen from the length
|
||||
/// (≤9 → font 5, else font 2, capping length at 20). When the caller passes
|
||||
/// (0, 0), the text is centered around cell (56, 15) for the chosen font.
|
||||
/// the <c>PlasmaPosText</c> layout logic (riovjoy2.cpp#L2235). Font 0 = auto:
|
||||
/// chosen from the length (≤9 → font 5, else font 2, capping length at 20).
|
||||
/// A nonzero <paramref name="font"/> is honored as given — the legacy code
|
||||
/// special-cased only its Score font (2); generalizing lets short text
|
||||
/// render small, e.g. digits inside a score box, which auto-fit never
|
||||
/// would. When the caller passes (0, 0), the text is centered around cell
|
||||
/// (56, 15) for the chosen font.
|
||||
/// </summary>
|
||||
public static (byte x, byte y, byte font, int length) ResolvePosText(
|
||||
string text, byte x, byte y, byte font)
|
||||
@@ -124,7 +127,7 @@ public static class PlasmaCommands
|
||||
if (text is null) throw new ArgumentNullException(nameof(text));
|
||||
int len = text.Length;
|
||||
|
||||
if (font != 2) // not the Score font
|
||||
if (font == 0) // auto-fit by length
|
||||
{
|
||||
if (len <= 9) font = 5;
|
||||
else { font = 2; if (len > 20) len = 20; }
|
||||
|
||||
@@ -47,6 +47,46 @@ public sealed class PlasmaDisplay
|
||||
public Task RowAsync(byte y, byte[] row, CancellationToken ct = default) =>
|
||||
WriteLockedAsync(new[] { PlasmaCommands.GraphicsRow(y, row) }, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Draw an outlined box with a blanked interior — the overlay chrome the
|
||||
/// original games drew for their rank|score field over the callsign
|
||||
/// (L4GAUGE.cpp's outlined 63×12 box). Pixel coordinates; a single
|
||||
/// graphics write (<c>ESC P</c>).
|
||||
///
|
||||
/// <para>The wire's graphics command addresses whole bytes horizontally,
|
||||
/// so the write covers the byte-aligned span containing
|
||||
/// <paramref name="x"/>..<paramref name="x"/>+<paramref name="w"/>-1;
|
||||
/// pixels inside that span but outside the box are cleared. Callers who
|
||||
/// care should place boxes on 8-px boundaries.</para>
|
||||
/// </summary>
|
||||
public Task BoxAsync(byte x, byte y, byte w, byte h, CancellationToken ct = default)
|
||||
{
|
||||
if (w == 0 || h == 0 || x + w > PlasmaCommands.Columns || y + h > PlasmaCommands.Rows)
|
||||
throw new ArgumentOutOfRangeException(nameof(w), "Box exceeds the 128x32 panel.");
|
||||
|
||||
int right = x + w - 1;
|
||||
int firstByte = x / 8;
|
||||
int lastByte = right / 8;
|
||||
int spanBytes = lastByte - firstByte + 1;
|
||||
|
||||
var data = new byte[spanBytes * h];
|
||||
for (int r = 0; r < h; r++)
|
||||
{
|
||||
bool edgeRow = r == 0 || r == h - 1;
|
||||
for (int px = firstByte * 8; px <= lastByte * 8 + 7; px++)
|
||||
{
|
||||
if (px < x || px > right)
|
||||
continue; // inside the byte span, outside the box: stays 0
|
||||
bool lit = edgeRow || px == x || px == right;
|
||||
if (lit)
|
||||
data[r * spanBytes + (px / 8 - firstByte)] |= (byte)(0x80 >> (px % 8));
|
||||
}
|
||||
}
|
||||
|
||||
return WriteLockedAsync(
|
||||
new[] { PlasmaCommands.GraphicsWrite(y, (byte)firstByte, (byte)spanBytes, h, data) }, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position the cursor, set attribute + font, and write text — the
|
||||
/// <c>PlasmaPosText</c> sequence (auto-fit via
|
||||
|
||||
@@ -219,6 +219,65 @@ public class FeedbackLineParserTests
|
||||
{
|
||||
Assert.NotEmpty(ParseError(line));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlasmaText_ThirdNumberWithTextFollowing_IsFont()
|
||||
{
|
||||
FeedbackCommand cmd = Parse("plasma text 35 21 2 \"1\"");
|
||||
Assert.Equal(35, cmd.X);
|
||||
Assert.Equal(21, cmd.Y);
|
||||
Assert.Equal(2, cmd.Font);
|
||||
Assert.Equal("1", cmd.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlasmaText_ThirdNumberAsLastToken_IsTextNotFont()
|
||||
{
|
||||
// `plasma text 2 2 7` keeps displaying "7", as it always has.
|
||||
FeedbackCommand cmd = Parse("plasma text 2 2 7");
|
||||
Assert.Equal(2, cmd.X);
|
||||
Assert.Equal(2, cmd.Y);
|
||||
Assert.Equal(0, cmd.Font);
|
||||
Assert.Equal("7", cmd.Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlasmaText_NoFontGiven_IsAuto()
|
||||
{
|
||||
Assert.Equal(0, Parse("plasma text 12 3 \"FUEL LOW\"").Font);
|
||||
Assert.Equal(0, Parse("plasma text \"VIPER 1-1\"").Font);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlasmaText_FontOutOfRange_Rejected()
|
||||
{
|
||||
Assert.Contains("font", ParseError("plasma text 2 2 99 \"X\""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlasmaBox_ParsesGeometry()
|
||||
{
|
||||
FeedbackCommand cmd = Parse("plasma box 32 19 64 12");
|
||||
Assert.Equal(FeedbackCommandKind.PlasmaBox, cmd.Kind);
|
||||
Assert.Equal(32, cmd.X);
|
||||
Assert.Equal(19, cmd.Y);
|
||||
Assert.Equal(64, cmd.Width);
|
||||
Assert.Equal(12, cmd.Height);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("plasma box")] // nothing
|
||||
[InlineData("plasma box 32 19 64")] // missing h
|
||||
[InlineData("plasma box 32 19 64 12 x")] // trailing token
|
||||
[InlineData("plasma box 128 0 1 1")] // x out of range
|
||||
[InlineData("plasma box 0 32 1 1")] // y out of range
|
||||
[InlineData("plasma box 100 0 40 1")] // spills off the right edge
|
||||
[InlineData("plasma box 0 28 1 8")] // spills off the bottom
|
||||
[InlineData("plasma box 0 0 0 5")] // zero width
|
||||
public void PlasmaBox_Malformed_ReturnsErrorText(string line)
|
||||
{
|
||||
Assert.NotEmpty(ParseError(line));
|
||||
}
|
||||
}
|
||||
|
||||
public class FeedbackLineBufferTests
|
||||
|
||||
@@ -146,6 +146,66 @@ public class FeedbackRouterTests : IDisposable
|
||||
Assert.Equal(2, router.DroppedCommands); // the two superseded middles
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlasmaText_CoalescesPerPosition_OtherFieldsSurvive()
|
||||
{
|
||||
// The documented multi-field layout sends several positioned texts in a
|
||||
// burst (callsign top, score bottom). Coalescing is per (x,y): a newer
|
||||
// score supersedes the queued score, never the queued callsign.
|
||||
var transport = new GatedTransport();
|
||||
var router = new FeedbackRouter();
|
||||
router.Attach(_scheduler, new RioInputMap(), new PlasmaDisplay(transport),
|
||||
new ProfileFeedbackConfig());
|
||||
|
||||
router.Dispatch(Text("PARK")); // goes busy, parked on the gate
|
||||
router.Dispatch(new FeedbackCommand
|
||||
{ Kind = FeedbackCommandKind.PlasmaText, Text = "VIPER", X = 2, Y = 2 });
|
||||
router.Dispatch(new FeedbackCommand
|
||||
{ Kind = FeedbackCommandKind.PlasmaText, Text = "SCORE 1", X = 2, Y = 18 });
|
||||
router.Dispatch(new FeedbackCommand
|
||||
{ Kind = FeedbackCommandKind.PlasmaText, Text = "SCORE 2", X = 2, Y = 18 }); // supersedes SCORE 1 only
|
||||
transport.Open();
|
||||
|
||||
var texts = new List<string>();
|
||||
for (int i = 0; i < 15; i++)
|
||||
{
|
||||
byte[] w = await transport.NextWriteAsync();
|
||||
string s = System.Text.Encoding.GetEncoding(28591).GetString(w);
|
||||
if (w.Length > 0 && w[0] != 0x1B)
|
||||
texts.Add(s); // the text chunks, minus ESC command prefixes
|
||||
if (texts.Count == 3)
|
||||
break;
|
||||
}
|
||||
Assert.Equal(new[] { "PARK", "VIPER", "SCORE 2" }, texts);
|
||||
Assert.Equal(1, router.DroppedCommands); // only SCORE 1 superseded
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlasmaBox_WritesOutlineWithBlankedInterior()
|
||||
{
|
||||
var transport = new GatedTransport();
|
||||
var router = new FeedbackRouter();
|
||||
router.Attach(_scheduler, new RioInputMap(), new PlasmaDisplay(transport),
|
||||
new ProfileFeedbackConfig());
|
||||
|
||||
// 16px-wide box at a byte boundary, 4 rows: bytes are exact.
|
||||
router.Dispatch(new FeedbackCommand
|
||||
{ Kind = FeedbackCommandKind.PlasmaBox, X = 32, Y = 10, Width = 16, Height = 4 });
|
||||
transport.Open();
|
||||
|
||||
byte[] w = await transport.NextWriteAsync();
|
||||
// ESC P s=0 y=10 x=4 w=2 h=4, then 8 data bytes.
|
||||
Assert.Equal(new byte[] { 0x1B, (byte)'P', 0, 10, 4, 2, 4 }, w.Take(7).ToArray());
|
||||
byte[] data = w.Skip(7).ToArray();
|
||||
Assert.Equal(new byte[]
|
||||
{
|
||||
0xFF, 0xFF, // top edge: all lit
|
||||
0x80, 0x01, // interior row: only the side walls
|
||||
0x80, 0x01,
|
||||
0xFF, 0xFF, // bottom edge
|
||||
}, data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PlasmaRows_StreamInFifoOrder_NotCoalesced()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user