Skip to content

Conversation

@eckern
Copy link

@eckern eckern commented Jul 16, 2025

User description

Boomerang_9282

This PR adds a new "Boomerang" platform type suitable for control of the aircraft shown above.

The boomerang platform uses a mixing mode where pitch and roll are applied according to the instantaneous position of each motor relative to the control platform.

In this mode the effect of the mixing table is altered; 'pitch' specifies the amplitude of a motor's cyclic mixing, and 'roll' specifies the phase, where 0-1 represents 0-360 degrees.

To support this an additional "rotor" sensor is added. This is intended to use a single digital input to implement a self-indexing encoder. The encoder track must consist of two or more ticks with uniformly spaced trailing edges. A single tick denoting the zero position must be exactly twice the width of all the other ticks. The rotor driver uses the system clock to make a first-order approximation of the exact instantaneous position.

The FURYF4OSD target has been modified, as a rotor encoder pin must be specified.

This is the code as-is running on the boomerang in the video

The boomerang can be printed in 160 grams of pla from models

TODO

  • the fast sin/cos are probably overkill optimization
  • it'd be neater to just rotate pitch/roll once then do all the mixing normally. ugh.
  • the mixer table is messy
  • polarity of rotor encoder should be configurable
  • direction of spin should be configurable
  • some nice art for the configurator ui

PR Type

New Feature


Description

This description is generated by an AI tool. It may have inaccuracies

  • Adds new PLATFORM_BOOMERANG platform type for FPV boomerang aircraft

  • Implements rotor sensor with self-indexing encoder for position tracking

  • Adds dynamic motor mixing based on instantaneous rotor position

  • Includes fast sin/cos approximation functions for real-time calculations


Changes diagram

flowchart LR
  A["Rotor Sensor"] --> B["Position Tracking"]
  B --> C["Dynamic Mixing"]
  C --> D["Motor Control"]
  E["PLATFORM_BOOMERANG"] --> F["State Management"]
  F --> C
Loading

Changes walkthrough 📝

Relevant files
Enhancement
6 files
runtime_config.h
Add BOOMERANG state flag                                                                 
+1/-0     
mixer.c
Implement boomerang dynamic mixing logic                                 
+51/-5   
mixer.h
Add PLATFORM_BOOMERANG enum value                                               
+2/-1     
initialisation.c
Initialize rotor sensor system                                                     
+5/-0     
rotor.c
New rotor sensor driver implementation                                     
+84/-0   
rotor.h
Rotor sensor API header                                                                   
+22/-0   
Configuration changes
3 files
target.h
Configure rotor pin for FURYF4OSD                                               
+3/-0     
CMakeLists.txt
Add rotor source files to build                                                   
+2/-0     
settings.yaml
Add BOOMERANG to platform type options                                     
+1/-1     

Need help?
  • Type /help how to ... in the comments thread for any questions about Qodo Merge usage.
  • Check out the documentation for more information.
  • @eckern eckern marked this pull request as ready for review July 16, 2025 01:57
    @qodo-merge-pro
    Copy link

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
    ⚡ Recommended focus areas for review

    Timing Issues

    The rotor sensor implementation uses micros() for timing calculations but lacks proper overflow handling and synchronization. The RPM calculation and angle interpolation could produce incorrect results during timer overflow or under high interrupt load.

    void rotorCallback(rotor_t *r) {
      timeDelta_t t = micros();
      if (!IORead(r->io)) {
        r->rise = t;
      } else {
        timeDelta_t width = t - r->rise;
        r->currentTick++;
        // Was big tick?
        if (width * 2 > r->width * 3) {
          r->circleTicks = r->currentTick;
          r->currentTick = 0;
          r->rpm = US_PER_MINUTE / (t - r->bigtick);
          r->bigtick = t;
        }
        r->width = width;
        r->interval = t - r->fall;
        r->fall = t;
      }
    }
    
    static void rotorInitX(IO_t io, rotor_t* r) {
      EXTIHandlerInit(&r->callback, (extiHandlerCallback*) rotorCallback); 
      EXTIConfig(io, &r->callback, 1, EXTI_Trigger_Rising_Falling);
      EXTIEnable(io, 1);
      r->io = io;
      r->circleTicks = 2;
    }
    
    static int rotorAngleX(rotor_t *r) {
      timeDelta_t interval = micros() - r->fall;
      if (interval > r->interval) {
        // expected another tick by now.  assume it's exactly there.
        return ((r->currentTick + 1) % r->circleTicks)
               * 360 / r->circleTicks;
      } else {
        return r->currentTick * 360 / r->circleTicks +
               interval * 360 / r->circleTicks / r->interval;
      }
    }
    
    static int rotorRPMX(rotor_t* r) {
      return r->rpm;
    }
    Missing Validation

    The boomerang mixing logic directly uses rotor angle and RPM values without validating sensor initialization or data validity. This could cause undefined behavior if the rotor sensor fails or provides invalid data during flight.

    	int16_t angle = 0;
        if (STATE(BOOMERANG)) {
            gvSet(0, rotorRPM());
            angle = rotorAngle();
        }
    
        // motors for non-servo mixes
        for (int i = 0; i < motorCount; i++) {
            if (STATE(BOOMERANG)) {
                // Note: yaw always stabilized; pitch/roll always raw. 
                rpyMix[i] = currentMixer[i].yaw * axisPID[YAW] +
                            currentMixer[i].pitch * (
                    -mulCosDegApprox(angle + 360 * currentMixer[i].roll, rcCommand[PITCH]) +
                    mulSinDegApprox(angle + 360 * currentMixer[i].roll, rcCommand[ROLL]));
            } 
    Code Quality

    The sine/cosine lookup table implementation and mixing logic contain inconsistent indentation, magic numbers, and lack proper bounds checking. The fast trigonometric functions may not provide sufficient accuracy for flight control applications.

    const uint8_t sinTab[90] = {0, 4, 8, 13, 17, 22, 26, 31, 35, 40, 44, 48, 53,
      57, 61, 66, 70, 74, 79, 83, 87, 91, 95, 100, 104, 108, 112, 116, 120, 124,
      127, 131, 135, 139, 143, 146, 150, 154, 157, 161, 164, 167, 171, 174, 177,
      181, 184, 187, 190, 193, 196, 198, 201, 204, 207, 209, 212, 214, 217, 219,
      221, 223, 226, 228, 230, 232, 233, 235, 237, 238, 240, 242, 243, 244, 246,
      247, 248, 249, 250, 251, 252, 252, 253, 254, 254, 255, 255, 255, 255, 255, };
    
    int32_t mulSinDegApprox(int32_t deg, int32_t v) {
      if (deg >= 360) deg = deg % 360;
      else if (deg < 0) deg = (deg % 360) + 360;
      if (deg == 90) return v;
      if (deg == 270) return -v;
      int32_t w;
      if (deg < 90)       w = +(int32_t)sinTab[deg];
      else if (deg < 180) w = +(int32_t)sinTab[180 - deg];
      else if (deg < 270) w = -(int32_t)sinTab[deg - 180];
      else                w = -(int32_t)sinTab[360 - deg];
      return v * w / 256;
    }
    
    int32_t mulCosDegApprox(int32_t deg, int32_t v) {
      return mulSinDegApprox(deg + 90, v);
    }

    @qodo-merge-pro
    Copy link

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Impact
    Possible issue
    Prevent division by zero error

    Add validation to prevent division by zero when calculating RPM. If t -
    r->bigtick equals zero, the division will cause undefined behavior and
    potentially crash the system.

    src/main/sensors/rotor.c [36-54]

     void rotorCallback(rotor_t *r) {
       timeDelta_t t = micros();
       if (!IORead(r->io)) {
         r->rise = t;
       } else {
         timeDelta_t width = t - r->rise;
         r->currentTick++;
         // Was big tick?
         if (width * 2 > r->width * 3) {
           r->circleTicks = r->currentTick;
           r->currentTick = 0;
    -      r->rpm = US_PER_MINUTE / (t - r->bigtick);
    +      timeDelta_t interval = t - r->bigtick;
    +      r->rpm = (interval > 0) ? US_PER_MINUTE / interval : 0;
           r->bigtick = t;
         }
         r->width = width;
         r->interval = t - r->fall;
         r->fall = t;
       }
     }
    • Apply / Chat
    Suggestion importance[1-10]: 8

    __

    Why: This correctly identifies a potential division-by-zero error when calculating r->rpm, which could cause a system crash, and provides a robust fix.

    Medium
    Add zero division protection

    Add validation to prevent division by zero when r->circleTicks or r->interval is
    zero. These divisions could cause system crashes during initialization or when
    no rotor ticks have been detected.

    src/main/sensors/rotor.c [64-74]

     static int rotorAngleX(rotor_t *r) {
    +  if (r->circleTicks == 0) return 0;
    +  
       timeDelta_t interval = micros() - r->fall;
       if (interval > r->interval) {
         // expected another tick by now.  assume it's exactly there.
         return ((r->currentTick + 1) % r->circleTicks)
                * 360 / r->circleTicks;
       } else {
    +    if (r->interval == 0) return 0;
         return r->currentTick * 360 / r->circleTicks +
                interval * 360 / r->circleTicks / r->interval;
       }
     }
    • Apply / Chat
    Suggestion importance[1-10]: 8

    __

    Why: This correctly identifies two potential division-by-zero errors with r->circleTicks and r->interval, preventing a potential system crash.

    Medium
    General
    Apply missing mixer scaling

    Apply mixerScale to the boomerang mixer calculation to maintain consistency with
    the standard mixer behavior. Without this scaling, the boomerang mode may have
    different response characteristics.

    src/main/flight/mixer.c [576-582]

     if (STATE(BOOMERANG)) {
         // Note: yaw always stabilized; pitch/roll always raw. 
    -    rpyMix[i] = currentMixer[i].yaw * axisPID[YAW] +
    -                currentMixer[i].pitch * (
    +    rpyMix[i] = (currentMixer[i].yaw * axisPID[YAW] +
    +                 currentMixer[i].pitch * (
             -mulCosDegApprox(angle + 360 * currentMixer[i].roll, rcCommand[PITCH]) +
    -        mulSinDegApprox(angle + 360 * currentMixer[i].roll, rcCommand[ROLL]));
    +        mulSinDegApprox(angle + 360 * currentMixer[i].roll, rcCommand[ROLL]))) * mixerScale;
     }
    • Apply / Chat
    Suggestion importance[1-10]: 7

    __

    Why: The suggestion correctly points out that mixerScale is not applied in the new BOOMERANG logic, which would cause inconsistent control response compared to other flight modes.

    Medium
    • More

    @eckern eckern marked this pull request as draft July 16, 2025 02:07
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    1 participant