CogniPilot

MODELICA LIBRARY · ENGINEERING REFERENCE

Vehicles/Rdd2/FlightModeSelector.mo

Download original .moGitHub source

Documentation: Vehicles.Rdd2.FlightModeSelector

within Vehicles.Rdd2;

// SPDX-License-Identifier: Apache-2.0

block FlightModeSelector
  "Decode a transmitter mode switch into the RDD2 flight-mode enumeration"

  parameter Real samplePeriod(unit = "s") = 0.01
    "Receiver service interval";
  parameter Real bandEdge_us[:] = {1200.0, 1800.0}
    "Strictly ascending band edges of the switch channel";
  parameter Real hysteresis_us(min = 0.0) = 25.0
    "Half-width of the ambiguous zone at each band edge";
  parameter Real debounceTime_s(unit = "s", min = 0.0) = 0.2
    "Continuous agreement required before a new band is accepted";
  parameter Real minimumChannel_us = 800.0
    "Below this the channel is a failed link, not a switch position";
  parameter Real maximumChannel_us = 2200.0
    "Above this the channel is a failed link, not a switch position";
  parameter Integer modeForPosition[size(bandEdge_us, 1) + 1] = {1, 3, 2}
    "Flight mode published for each band, low to high";

  final parameter Integer bandCount = size(bandEdge_us, 1) + 1;
  final parameter Integer debounceSamples(min = 1) =
    max(1, integer(ceil(debounceTime_s / samplePeriod)))
    "Samples of agreement equivalent to debounceTime_s";

  input Real switchPwm_us "Raw mode-switch channel, microseconds";
  output Integer mode(start = modeForPosition[1], fixed = true)
    "0=acro, 1=attitude, 2=mission, 3=pilot position";

protected
  discrete Integer position(start = 0, fixed = true)
    "Accepted band index; 0 until the first valid sample seeds it";
  discrete Integer pending(start = 0, fixed = true)
    "Band index currently accumulating agreement";
  discrete Integer agreement(start = 0, fixed = true)
    "Consecutive samples the pending band has been observed";
  discrete Integer candidate(start = 0, fixed = true)
    "Band the hysteretic quantizer reports this sample";
  discrete Boolean channelValid(start = false, fixed = true);

equation
  assert(samplePeriod > 0.0,
    "FlightModeSelector sample period must be positive");
  assert(maximumChannel_us > minimumChannel_us,
    "FlightModeSelector channel validity window is empty");
  for index in 1:size(bandEdge_us, 1) loop
    assert(bandEdge_us[index] > minimumChannel_us
      and bandEdge_us[index] < maximumChannel_us,
      "FlightModeSelector band edge lies outside the validity window");
  end for;
  // Hysteresis wider than half the narrowest band would let two bands claim
  // the same channel value, which makes the accepted position depend on the
  // path taken rather than on the switch.
  for index in 2:size(bandEdge_us, 1) loop
    assert(bandEdge_us[index] - bandEdge_us[index - 1] > 2.0 * hysteresis_us,
      "FlightModeSelector hysteresis is at least half the narrowest band");
  end for;

algorithm
  when sample(0.0, samplePeriod) then
    channelValid := switchPwm_us > minimumChannel_us
      and switchPwm_us < maximumChannel_us;
    candidate := Vehicles.Interfaces.switchBandPosition(
      switchPwm_us, bandEdge_us, hysteresis_us, max(1, pre(position)));

    if not channelValid then
      // A failed link holds the mode the pilot last selected. Falling back to
      // a default here would change flight mode on a single dropped frame.
      position := pre(position);
      pending := pre(position);
      agreement := 0;
    elseif pre(position) == 0 then
      // Seed from the first valid sample without announcing a change, as
      // ArduPilot seeds a switch on its first radio read. The seed uses the
      // memoryless quantizer because there is no previous position yet.
      position := Vehicles.Interfaces.switchBandPosition(
        switchPwm_us, bandEdge_us, 0.0, 1);
      pending := position;
      agreement := 0;
    elseif candidate == pre(position) then
      // Agreement with the held band restarts the debounce, so a switch that
      // bounces back through its current position stays put.
      position := pre(position);
      pending := pre(position);
      agreement := 0;
    elseif candidate == pre(pending) then
      agreement := pre(agreement) + 1;
      position := if agreement >= debounceSamples then candidate
        else pre(position);
      pending := candidate;
    else
      position := pre(position);
      pending := candidate;
      agreement := 1;
    end if;

    mode := modeForPosition[position];
  end when;

  annotation(Documentation(info = "<html>
    <p>This is the RDD2 twin of ArduPilot's <code>FLTMODE_CH</code> decode and
    PX4's <code>RC_MAP_FLTMODE</code> slot mapping: one continuous receiver
    channel, quantized into bands, mapped through a table into the flight-mode
    enumeration the guidance task already consumes.</p>
    <h4>Why both hysteresis and a debounce</h4>
    <p>No production autopilot uses amplitude hysteresis on the mode channel;
    all three surveyed projects use hard thresholds and reject glitches with a
    time filter (ArduPilot 200 ms of continuous agreement, PX4 two identical
    frames, Betaflight none). A time filter is the right answer for impulsive
    noise but does nothing for a switch resting on a band edge, where it merely
    delays each chatter by the debounce interval. Hysteresis is the right
    answer for that case and does nothing for glitch frames. The two are
    orthogonal, so this block applies both:
    <code>Vehicles.Interfaces.switchBandPosition</code> supplies the amplitude
    half and the counter here supplies the temporal half.</p>
    <h4>Mode table</h4>
    <p>The default table maps a three-position switch low to high onto
    attitude, pilot position, and mission, matching the conventional PX4
    Stabilized / Position / Mission ordering. Only modes RDD2 actually
    implements appear; acro remains reachable by configuring
    <code>modeForPosition</code>, because a three-position switch cannot carry
    four modes.</p>
    <h4>Timing</h4>
    <p>All state advances on one <code>sample</code> clock and every history
    reference is an explicit <code>pre</code>, so the decode is a pure
    synchronous function of this sample and the previous accepted state, with
    no reliance on implicit delay.</p>
  </html>"));
end FlightModeSelector;