Phase 4: axis calibration + plasma display (RioJoy.Core)
Port the analog calibration math and the plasma/VFD command set: - Calibration/: AxisCalibrator ports UpdateThrottle/UpdatePadal/UpdateJoystick (riovjoy2.cpp#L1504+) — throttle deadzone + ratchet field, pedal deadzones, joystick X/Y auto-ranging from observed min/max, rudder mixing (enableZR), and the per-axis invert flags. Stateful (start positions, last outputs, observed extremes) like the legacy globals, with the RIOcmd axis resets. Final outputs clamp to the documented 0..32766 range (also guards a legacy compounding quirk). AxisOutputs carries the six values; IJoystickSink gains SetAxis(JoyAxis,value) so calibrated axes reach the HID feeder. - Plasma/: PlasmaCommands builds the CPlasma ESC sequences (clear/cursor/font/ attr/box draw+fill/text) + GetFontSize + the PlasmaPosText auto-fit/centering; PlasmaDisplay writes them over the secondary COM transport. - tests: 21 new xUnit tests (105 total) for throttle proportional/saturation/ invert, pedal ZR mix vs. direct, joystick centering/direction/invert/reset, the output clamp, and plasma byte sequences/font sizes/auto-fit positioning. Hardware verification of axis feel + plasma output remains; the game-specific PlasmaScoreDraw layout is deferred to profile content (Phase 5/7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
namespace RioJoy.Core.Calibration;
|
||||
|
||||
/// <summary>
|
||||
/// Per-profile axis calibration options. Defaults mirror the legacy
|
||||
/// <c>JoyStick</c> ini section (riovjoy2.cpp#L345): all inverts off,
|
||||
/// <see cref="EnableZR"/> on. When <see cref="EnableZR"/> is set, the pedals are
|
||||
/// mixed into a single rudder axis (Rz) and Rx/Ry are held centered; when clear,
|
||||
/// the pedals drive Rx/Ry directly and Rz is held centered.
|
||||
/// </summary>
|
||||
public sealed record AxisCalibrationConfig
|
||||
{
|
||||
public bool InvertX { get; init; }
|
||||
public bool InvertY { get; init; }
|
||||
public bool InvertZ { get; init; }
|
||||
public bool InvertXR { get; init; }
|
||||
public bool InvertYR { get; init; }
|
||||
public bool InvertZR { get; init; }
|
||||
|
||||
/// <summary>Mix the two pedals into a rudder axis (Rz). Legacy default: on.</summary>
|
||||
public bool EnableZR { get; init; } = true;
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
using RioJoy.Core.Protocol;
|
||||
|
||||
namespace RioJoy.Core.Calibration;
|
||||
|
||||
/// <summary>
|
||||
/// Converts raw RIO analog samples into calibrated virtual-joystick axis values.
|
||||
/// Stateful port of <c>UpdateThrottle</c>/<c>UpdatePadal</c>/<c>UpdateJoystick</c>
|
||||
/// (riovjoy2.cpp#L1504+); see docs/PROTOCOL.md §4. State (auto-ranging start
|
||||
/// positions, observed min/max for X/Y, last outputs, throttle ratchet) carries
|
||||
/// across <see cref="Update"/> calls exactly as the legacy globals did.
|
||||
///
|
||||
/// <para>Final outputs are clamped to <c>0..<see cref="AxisOutputs.Max"/></c> — the
|
||||
/// documented axis range — which also guards a legacy quirk where a value pinned
|
||||
/// at its observed extreme could compound across polls (see ⚠️ below).</para>
|
||||
/// </summary>
|
||||
public sealed class AxisCalibrator
|
||||
{
|
||||
private const int RangeThrottle = 800;
|
||||
private const int DeadzoneThrottle = 50;
|
||||
private const int RangePadal = 500;
|
||||
private const int DeadzonePadal = 10;
|
||||
private const int DeadzoneJoystick = 5;
|
||||
|
||||
private readonly AxisCalibrationConfig _config;
|
||||
|
||||
// Auto-ranging start positions.
|
||||
private int _throttleStart = int.MinValue;
|
||||
private int _leftPedalStart = int.MaxValue;
|
||||
private int _rightPedalStart = int.MaxValue;
|
||||
|
||||
// Last computed (pre-clamp) outputs — persist across calls like the legacy globals.
|
||||
private int _throttleLast = AxisOutputs.Center;
|
||||
private int _leftPedalLast = AxisOutputs.Center;
|
||||
private int _rightPedalLast = AxisOutputs.Center;
|
||||
private int _joystickXLast = AxisOutputs.Center;
|
||||
private int _joystickYLast = AxisOutputs.Center;
|
||||
|
||||
// Throttle ratchet direction. Initialized -1; the legacy never sets it
|
||||
// positive, so the "back" branches are effectively dead but ported faithfully.
|
||||
private int _throttleResult = -1;
|
||||
|
||||
// Observed min/max for the joystick axes (auto-ranging rate).
|
||||
private int _minX, _maxX, _minY, _maxY;
|
||||
|
||||
public AxisCalibrator(AxisCalibrationConfig? config = null)
|
||||
{
|
||||
_config = config ?? new AxisCalibrationConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calibrate one raw analog sample and return the six axis values. Updates the
|
||||
/// observed X/Y min/max first (as the legacy <c>AnalogEvent</c> did), then runs
|
||||
/// the per-axis math.
|
||||
/// </summary>
|
||||
public AxisOutputs Update(AnalogReport raw)
|
||||
{
|
||||
_maxX = Math.Max(raw.JoystickX, _maxX);
|
||||
_minX = Math.Min(raw.JoystickX, _minX);
|
||||
_maxY = Math.Max(raw.JoystickY, _maxY);
|
||||
_minY = Math.Min(raw.JoystickY, _minY);
|
||||
|
||||
int z = Throttle(raw.Throttle);
|
||||
(int rx, int ry, int rz) = Pedals(raw.LeftPedal, raw.RightPedal);
|
||||
int x = JoystickX(raw.JoystickX);
|
||||
int y = JoystickY(raw.JoystickY);
|
||||
|
||||
return new AxisOutputs(
|
||||
Clamp(x), Clamp(y), Clamp(z), Clamp(rx), Clamp(ry), Clamp(rz));
|
||||
}
|
||||
|
||||
private static int Clamp(int v) => Math.Clamp(v, 0, AxisOutputs.Max);
|
||||
|
||||
private int Throttle(int throttle)
|
||||
{
|
||||
if (throttle < -RangeThrottle || throttle > RangeThrottle)
|
||||
{
|
||||
if (throttle < RangeThrottle)
|
||||
_throttleLast = _throttleResult > 0 ? 1000 : -1000;
|
||||
else
|
||||
_throttleLast = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_throttleStart < throttle)
|
||||
_throttleStart = throttle;
|
||||
|
||||
int lT = _throttleStart - throttle;
|
||||
if (lT > 800)
|
||||
lT = 800;
|
||||
|
||||
if (lT != 0)
|
||||
{
|
||||
lT = lT * 1000 / 800;
|
||||
if (lT is > -DeadzoneThrottle and < DeadzoneThrottle)
|
||||
_throttleLast = 0;
|
||||
else if (_throttleResult > 0) // back
|
||||
_throttleLast = 900 + (lT * _throttleResult / 10);
|
||||
else // front
|
||||
_throttleLast = lT * _throttleResult;
|
||||
}
|
||||
// lT == 0 leaves _throttleLast unchanged (legacy behavior).
|
||||
}
|
||||
|
||||
_throttleLast = Math.Abs(_throttleLast * 32);
|
||||
return _config.InvertZ ? AxisOutputs.Max - _throttleLast : _throttleLast;
|
||||
}
|
||||
|
||||
private (int rx, int ry, int rz) Pedals(int leftPedal, int rightPedal)
|
||||
{
|
||||
// LEFT pedal
|
||||
if (leftPedal < -RangePadal || leftPedal > RangePadal)
|
||||
{
|
||||
_leftPedalLast = leftPedal > RangePadal ? -1000 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_leftPedalStart > leftPedal)
|
||||
_leftPedalStart = leftPedal;
|
||||
|
||||
int lP = _leftPedalStart - leftPedal;
|
||||
if (lP != 0)
|
||||
lP = lP * 1000 / RangePadal;
|
||||
if (Math.Abs(lP) < DeadzonePadal)
|
||||
lP = 0;
|
||||
_leftPedalLast = lP;
|
||||
}
|
||||
_leftPedalLast = Math.Abs(_leftPedalLast * 32);
|
||||
if (_config.InvertXR)
|
||||
_leftPedalLast = AxisOutputs.Max - _leftPedalLast;
|
||||
|
||||
// RIGHT pedal
|
||||
if (rightPedal < -RangePadal || rightPedal > RangePadal)
|
||||
{
|
||||
_rightPedalLast = rightPedal > RangePadal ? 1000 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_rightPedalStart > rightPedal)
|
||||
_rightPedalStart = rightPedal;
|
||||
|
||||
int lP = _rightPedalStart - rightPedal;
|
||||
if (lP != 0)
|
||||
lP = lP * 1000 / RangePadal;
|
||||
if (Math.Abs(lP) < DeadzonePadal)
|
||||
lP = 0;
|
||||
_rightPedalLast = -lP;
|
||||
}
|
||||
_rightPedalLast = Math.Abs(_rightPedalLast * 32);
|
||||
if (_config.InvertYR)
|
||||
_rightPedalLast = AxisOutputs.Max - _rightPedalLast;
|
||||
|
||||
int rudder = AxisOutputs.Center - (_leftPedalLast / 2) + (_rightPedalLast / 2);
|
||||
if (_config.InvertZR)
|
||||
rudder = AxisOutputs.Max - rudder;
|
||||
|
||||
// EnableZR mixes the pedals into the rudder (Rz) and centers Rx/Ry;
|
||||
// otherwise the pedals drive Rx/Ry directly and Rz is centered.
|
||||
int rx = _config.EnableZR ? AxisOutputs.Center : _leftPedalLast;
|
||||
int ry = _config.EnableZR ? AxisOutputs.Center : _rightPedalLast;
|
||||
int rz = _config.EnableZR ? rudder : AxisOutputs.Center;
|
||||
return (rx, ry, rz);
|
||||
}
|
||||
|
||||
private int JoystickX(int joystickX)
|
||||
{
|
||||
int startingLeftRate = Math.Min(_minX, -80);
|
||||
int startingRightRate = Math.Max(_maxX, 80);
|
||||
int sLeftRate = AxisOutputs.Center / (Math.Abs(startingLeftRate) - DeadzoneJoystick);
|
||||
int sRightRate = AxisOutputs.Center / (startingRightRate - DeadzoneJoystick);
|
||||
|
||||
int lJx = joystickX;
|
||||
if (lJx < 0) // LEFT
|
||||
{
|
||||
lJx += DeadzoneJoystick;
|
||||
_joystickXLast = lJx < 0 ? AxisOutputs.Center - (lJx * sLeftRate) : AxisOutputs.Center;
|
||||
}
|
||||
else if (lJx > 0) // RIGHT
|
||||
{
|
||||
lJx -= DeadzoneJoystick;
|
||||
// 16838 (not 16383) and a +2 nudge are deliberate legacy anti-snap tweaks.
|
||||
_joystickXLast = lJx > 0 ? 16838 - ((lJx + 2) * sRightRate) : AxisOutputs.Center;
|
||||
}
|
||||
// lJx == 0 leaves _joystickXLast unchanged (legacy behavior).
|
||||
|
||||
return _config.InvertX ? AxisOutputs.Max - _joystickXLast : _joystickXLast;
|
||||
}
|
||||
|
||||
private int JoystickY(int joystickY)
|
||||
{
|
||||
int startingUpRate = Math.Min(_minY, -80);
|
||||
int startingDownRate = Math.Max(_maxY, 80);
|
||||
int sUpRate = AxisOutputs.Center / (Math.Abs(startingUpRate) - DeadzoneJoystick);
|
||||
int sDownRate = AxisOutputs.Center / (startingDownRate - DeadzoneJoystick);
|
||||
|
||||
int lJy = joystickY;
|
||||
if (lJy < 0) // UP
|
||||
{
|
||||
lJy += DeadzoneJoystick;
|
||||
_joystickYLast = lJy < 0 ? AxisOutputs.Center - (lJy * sUpRate) : AxisOutputs.Center;
|
||||
}
|
||||
else if (lJy > 0) // DOWN
|
||||
{
|
||||
lJy -= DeadzoneJoystick;
|
||||
_joystickYLast = lJy > 0 ? AxisOutputs.Center - (lJy * sDownRate) : AxisOutputs.Center;
|
||||
}
|
||||
// lJy == 0 leaves _joystickYLast unchanged (legacy behavior).
|
||||
|
||||
return _config.InvertY ? AxisOutputs.Max - _joystickYLast : _joystickYLast;
|
||||
}
|
||||
|
||||
// --- Resets (port of the RIOcmd axis-reset cases, riovjoy2.cpp#L1852) -------
|
||||
|
||||
/// <summary>Dispatch a calibration reset for a RIO-command reset code.</summary>
|
||||
public void Reset(Mapping.RioCommandCode command)
|
||||
{
|
||||
switch (command)
|
||||
{
|
||||
case Mapping.RioCommandCode.ResetAllAxes: ResetAll(); break;
|
||||
case Mapping.RioCommandCode.ResetThrottle: ResetThrottle(); break;
|
||||
case Mapping.RioCommandCode.ResetLeftPedal: ResetLeftPedal(); break;
|
||||
case Mapping.RioCommandCode.ResetRightPedal: ResetRightPedal(); break;
|
||||
case Mapping.RioCommandCode.ResetVerticalJoystick: ResetVerticalJoystick(); break;
|
||||
case Mapping.RioCommandCode.ResetHorizontalJoystick: ResetHorizontalJoystick(); break;
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetAll()
|
||||
{
|
||||
_throttleResult = -1;
|
||||
_throttleStart = int.MinValue;
|
||||
_leftPedalStart = int.MaxValue;
|
||||
_rightPedalStart = int.MaxValue;
|
||||
_joystickXLast = AxisOutputs.Center;
|
||||
_joystickYLast = AxisOutputs.Center;
|
||||
_throttleLast = 0;
|
||||
_leftPedalLast = 0;
|
||||
_rightPedalLast = 0;
|
||||
_minX = _maxX = _minY = _maxY = 0;
|
||||
}
|
||||
|
||||
public void ResetThrottle()
|
||||
{
|
||||
_throttleResult = -1;
|
||||
_throttleStart = int.MinValue;
|
||||
_throttleLast = 0;
|
||||
}
|
||||
|
||||
public void ResetLeftPedal()
|
||||
{
|
||||
_leftPedalStart = int.MaxValue;
|
||||
_leftPedalLast = 0;
|
||||
}
|
||||
|
||||
public void ResetRightPedal()
|
||||
{
|
||||
_rightPedalStart = int.MaxValue;
|
||||
_rightPedalLast = 0;
|
||||
}
|
||||
|
||||
public void ResetVerticalJoystick()
|
||||
{
|
||||
_joystickYLast = AxisOutputs.Center;
|
||||
_maxY = _minY = 0;
|
||||
}
|
||||
|
||||
public void ResetHorizontalJoystick()
|
||||
{
|
||||
_joystickXLast = AxisOutputs.Center;
|
||||
_maxX = _minX = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace RioJoy.Core.Calibration;
|
||||
|
||||
/// <summary>
|
||||
/// The six calibrated virtual-joystick axis values, each in the documented range
|
||||
/// <c>0..32766</c> (center 16383). Output of <see cref="AxisCalibrator"/>; fed to
|
||||
/// the virtual HID device (<c>HID_USAGE_X..RZ</c> in the legacy).
|
||||
/// </summary>
|
||||
public readonly struct AxisOutputs
|
||||
{
|
||||
/// <summary>Documented axis range maximum (legacy uses 0..32766).</summary>
|
||||
public const int Max = 32766;
|
||||
|
||||
/// <summary>Documented axis center value.</summary>
|
||||
public const int Center = 16383;
|
||||
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public int Z { get; }
|
||||
public int Rx { get; }
|
||||
public int Ry { get; }
|
||||
public int Rz { get; }
|
||||
|
||||
public AxisOutputs(int x, int y, int z, int rx, int ry, int rz)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
Rx = rx;
|
||||
Ry = ry;
|
||||
Rz = rz;
|
||||
}
|
||||
|
||||
public override string ToString() => $"X:{X} Y:{Y} Z:{Z} Rx:{Rx} Ry:{Ry} Rz:{Rz}";
|
||||
}
|
||||
|
||||
/// <summary>The six virtual-joystick axes.</summary>
|
||||
public enum JoyAxis
|
||||
{
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
Rx,
|
||||
Ry,
|
||||
Rz,
|
||||
}
|
||||
@@ -66,13 +66,16 @@ public interface IInputSink
|
||||
|
||||
/// <summary>
|
||||
/// Virtual joystick output (the HID feeder → RioGamepad driver in Phase 1).
|
||||
/// Button numbers are 0-based into the 96-button report; the hat is the single POV.
|
||||
/// Button numbers are 0-based into the 96-button report; the hat is the single POV;
|
||||
/// axis values are in the range <c>0..32766</c> (see <c>AxisOutputs</c>).
|
||||
/// </summary>
|
||||
public interface IJoystickSink
|
||||
{
|
||||
void SetButton(int button, bool pressed);
|
||||
|
||||
void SetHat(RioHat position);
|
||||
|
||||
void SetAxis(Calibration.JoyAxis axis, int value);
|
||||
}
|
||||
|
||||
/// <summary>Lamp (lighted-button) feedback, sent back to the RIO over serial.</summary>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.Text;
|
||||
|
||||
namespace RioJoy.Core.Plasma;
|
||||
|
||||
/// <summary>A glyph cell size in pixels (width × height).</summary>
|
||||
public readonly record struct FontSize(int Width, int Height);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the ESC-based command byte sequences for the plasma / VFD text display
|
||||
/// on the secondary COM port. Pure port of the <c>CPlasma</c> command methods
|
||||
/// (riovjoy2.cpp#L2146); see docs/PROTOCOL.md §7. Text is encoded as Latin-1
|
||||
/// (each char → one byte), matching the legacy 8-bit <c>char*</c> packets.
|
||||
/// </summary>
|
||||
public static class PlasmaCommands
|
||||
{
|
||||
/// <summary>The ESC lead byte (legacy <c>#define ESC 27</c>).</summary>
|
||||
public const byte Esc = 27;
|
||||
|
||||
/// <summary>Clear the display (<c>ESC @</c>).</summary>
|
||||
public static byte[] Clear() => new[] { Esc, (byte)'@' };
|
||||
|
||||
/// <summary>Move the cursor home (<c>ESC L</c>).</summary>
|
||||
public static byte[] CursorHome() => new[] { Esc, (byte)'L' };
|
||||
|
||||
/// <summary>Set cursor cell (<c>ESC G n</c>).</summary>
|
||||
public static byte[] Cursor(byte n) => new[] { Esc, (byte)'G', n };
|
||||
|
||||
/// <summary>Set cursor X (<c>ESC R x</c>).</summary>
|
||||
public static byte[] CursorX(byte x) => new[] { Esc, (byte)'R', x };
|
||||
|
||||
/// <summary>Set cursor Y (<c>ESC Q y</c>).</summary>
|
||||
public static byte[] CursorY(byte y) => new[] { Esc, (byte)'Q', y };
|
||||
|
||||
/// <summary>Set font attribute (<c>ESC H attr</c>).</summary>
|
||||
public static byte[] FontAttr(byte attr) => new[] { Esc, (byte)'H', attr };
|
||||
|
||||
/// <summary>Select font (<c>ESC K font</c>).</summary>
|
||||
public static byte[] Font(byte font) => new[] { Esc, (byte)'K', font };
|
||||
|
||||
/// <summary>Draw a box outline (<c>ESC X l t r b</c>).</summary>
|
||||
public static byte[] BoxDraw(byte left, byte top, byte right, byte bottom) =>
|
||||
new[] { Esc, (byte)'X', left, top, right, bottom };
|
||||
|
||||
/// <summary>Fill a box (<c>ESC x 0 l t r b</c>).</summary>
|
||||
public static byte[] BoxFill(byte left, byte top, byte right, byte bottom) =>
|
||||
new[] { Esc, (byte)'x', (byte)0, left, top, right, bottom };
|
||||
|
||||
/// <summary>Encode display text as raw bytes (Latin-1, one byte per char).</summary>
|
||||
public static byte[] Text(string text)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(text);
|
||||
return Encoding.Latin1.GetBytes(text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Glyph cell size for a font id. Port of <c>GetFontSize</c> (riovjoy2.cpp#L2198):
|
||||
/// fonts 0–3 and 6–7 are 5×7, fonts 4–5 are 10×14.
|
||||
/// </summary>
|
||||
public static FontSize GetFontSize(int font) => font switch
|
||||
{
|
||||
4 or 5 => new FontSize(10, 14),
|
||||
_ => new FontSize(5, 7),
|
||||
};
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public static (byte x, byte y, byte font, int length) ResolvePosText(
|
||||
string text, byte x, byte y, byte font)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(text);
|
||||
int len = text.Length;
|
||||
|
||||
if (font != 2) // not the Score font
|
||||
{
|
||||
if (len <= 9) font = 5;
|
||||
else { font = 2; if (len > 20) len = 20; }
|
||||
}
|
||||
else if (len > 20)
|
||||
{
|
||||
len = 20;
|
||||
}
|
||||
|
||||
if (x == 0 && y == 0)
|
||||
{
|
||||
FontSize size = GetFontSize(font);
|
||||
x = (byte)(56 - (len * size.Width / 2));
|
||||
y = (byte)(15 - (size.Height / 2));
|
||||
}
|
||||
|
||||
return (x, y, font, len);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using RioJoy.Core.Serial;
|
||||
|
||||
namespace RioJoy.Core.Plasma;
|
||||
|
||||
/// <summary>
|
||||
/// Drives the plasma / VFD text display over its (secondary) serial transport,
|
||||
/// writing the ESC sequences built by <see cref="PlasmaCommands"/>. Thin async
|
||||
/// wrapper around an <see cref="IRioTransport"/>; the display is write-only. The
|
||||
/// content shown is per-profile (Phase 5+).
|
||||
/// </summary>
|
||||
public sealed class PlasmaDisplay
|
||||
{
|
||||
private readonly IRioTransport _transport;
|
||||
|
||||
public PlasmaDisplay(IRioTransport transport)
|
||||
{
|
||||
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
||||
}
|
||||
|
||||
public Task ClearAsync(CancellationToken ct = default) =>
|
||||
WriteAsync(PlasmaCommands.Clear(), ct);
|
||||
|
||||
public Task CursorHomeAsync(CancellationToken ct = default) =>
|
||||
WriteAsync(PlasmaCommands.CursorHome(), ct);
|
||||
|
||||
public Task TextAsync(string text, CancellationToken ct = default) =>
|
||||
WriteAsync(PlasmaCommands.Text(text), ct);
|
||||
|
||||
/// <summary>
|
||||
/// Position the cursor, set attribute + font, and write text — the
|
||||
/// <c>PlasmaPosText</c> sequence (auto-fit via
|
||||
/// <see cref="PlasmaCommands.ResolvePosText"/>). Pass (0,0) to auto-center.
|
||||
/// </summary>
|
||||
public async Task PosTextAsync(
|
||||
string text, byte x = 0, byte y = 0, byte attr = 0, byte font = 0,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
(byte rx, byte ry, byte rfont, int len) = PlasmaCommands.ResolvePosText(text, x, y, font);
|
||||
|
||||
await WriteAsync(PlasmaCommands.CursorX(rx), ct).ConfigureAwait(false);
|
||||
await WriteAsync(PlasmaCommands.CursorY(ry), ct).ConfigureAwait(false);
|
||||
await WriteAsync(PlasmaCommands.FontAttr(attr), ct).ConfigureAwait(false);
|
||||
await WriteAsync(PlasmaCommands.Font(rfont), ct).ConfigureAwait(false);
|
||||
await WriteAsync(PlasmaCommands.Text(text[..len]), ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private Task WriteAsync(byte[] data, CancellationToken ct) =>
|
||||
_transport.WriteAsync(data, ct).AsTask();
|
||||
}
|
||||
Reference in New Issue
Block a user