using RioJoy.Core.Calibration;
using RioJoy.Core.Mapping;
using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;
namespace RioJoy.Core;
///
/// Ties the live RIO link to the input/output pipeline for one active profile:
/// serial button/keypad packets drive the ; analog
/// replies drive the and the six joystick axes; RIO
/// commands trigger calibration resets and serial requests. This is the runtime
/// assembled per-profile by the tray app (Phase 5).
///
public sealed class RioRuntime : IRioCommandSink, IDisposable
{
private readonly RioSerialLink _link;
private readonly IJoystickSink _joystick;
private readonly AxisCalibrator _calibrator;
private readonly InputRouter _router;
private bool _started;
public RioRuntime(
RioSerialLink link,
RioInputMap map,
IInputSink input,
IJoystickSink joystick,
AxisCalibrator? calibrator = null)
{
_link = link ?? throw new ArgumentNullException(nameof(link));
ArgumentNullException.ThrowIfNull(map);
ArgumentNullException.ThrowIfNull(input);
_joystick = joystick ?? throw new ArgumentNullException(nameof(joystick));
_calibrator = calibrator ?? new AxisCalibrator();
var lamp = new SerialLampSink(link);
_router = new InputRouter(map, input, joystick, lamp, this);
}
/// Raised when a diagnostic toggle RIO command fires (raw-axes / poll-rate).
public event Action? DiagnosticToggle;
/// Subscribe to the link and set all lamps to their idle state.
public void Start()
{
if (_started)
return;
_started = true;
_link.PacketReceived += OnPacket;
_link.AnalogReceived += OnAnalog;
_router.InitializeLamps();
}
/// Unsubscribe from the link.
public void Stop()
{
if (!_started)
return;
_started = false;
_link.PacketReceived -= OnPacket;
_link.AnalogReceived -= OnAnalog;
}
public void Dispose() => Stop();
/// Invoke a RIO command directly (e.g. from the tray menu).
public void Trigger(RioCommandCode command) => ((IRioCommandSink)this).Execute(command);
private void OnAnalog(AnalogReport report)
{
AxisOutputs axes = _calibrator.Update(report);
_joystick.SetAxis(JoyAxis.X, axes.X);
_joystick.SetAxis(JoyAxis.Y, axes.Y);
_joystick.SetAxis(JoyAxis.Z, axes.Z);
_joystick.SetAxis(JoyAxis.Rx, axes.Rx);
_joystick.SetAxis(JoyAxis.Ry, axes.Ry);
_joystick.SetAxis(JoyAxis.Rz, axes.Rz);
}
private void OnPacket(RioPacket packet)
{
ReadOnlySpan p = packet.Payload.Span;
switch (packet.Command)
{
case RioCommand.ButtonPressed when p[0] < RioAddress.ButtonCount:
_router.Press(RioAddress.FromButton(p[0]));
break;
case RioCommand.ButtonReleased when p[0] < RioAddress.ButtonCount:
_router.Release(RioAddress.FromButton(p[0]));
break;
case RioCommand.KeyPressed when IsKeypad(p):
_router.Press(RioAddress.FromKeypad(p[0], p[1]));
break;
case RioCommand.KeyReleased when IsKeypad(p):
_router.Release(RioAddress.FromKeypad(p[0], p[1]));
break;
}
}
private static bool IsKeypad(ReadOnlySpan p) => p[0] is 0 or 1 && p[1] <= 0x0F;
// IRioCommandSink: RIO commands routed from a button (RIOcmd port).
void IRioCommandSink.Execute(RioCommandCode command)
{
switch (command)
{
case RioCommandCode.ResetAllAxes:
case RioCommandCode.ResetThrottle:
case RioCommandCode.ResetLeftPedal:
case RioCommandCode.ResetRightPedal:
case RioCommandCode.ResetVerticalJoystick:
case RioCommandCode.ResetHorizontalJoystick:
_calibrator.Reset(command);
FireAndForget(_link.ResetAsync((RioResetTarget)(byte)command));
break;
case RioCommandCode.GeneralReset:
FireAndForget(_link.ResetAsync(RioResetTarget.All));
_router.InitializeLamps();
break;
case RioCommandCode.RequestVersionAndCheck:
FireAndForget(_link.RequestVersionAsync());
FireAndForget(_link.RequestCheckAsync());
break;
case RioCommandCode.ToggleRawAxesReadout:
case RioCommandCode.TogglePollRateReadout:
DiagnosticToggle?.Invoke(command);
break;
}
}
private static void FireAndForget(Task task) =>
task.ContinueWith(static t => _ = t.Exception, TaskContinuationOptions.OnlyOnFaulted);
}