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); }