Skip to content

Conversation

@sensei-hacker
Copy link
Member

@sensei-hacker sensei-hacker commented Aug 6, 2024

Automatically detects and sets FC and compass alignment by having the user point the aircraft north and lift the nose.

alignment-tool-01.mp4

Not ready for merge. The PR is just to share the changes with Darkwingduck or other interested parties.

@sensei-hacker sensei-hacker added Enhancement Enhancement of existing functions Don't merge labels Aug 6, 2024
@sensei-hacker
Copy link
Member Author

sensei-hacker commented Nov 9, 2025

@shota3527 You and maybe @breadoven may be the only two people who understand the math needed to get two parts of this production-ready. Currently it's only POC level. Maybe you can provide some guidance on the math?

This PR implements a wizard that has the user turn the plane 90 degrees, then lift the nose. Then based on the IMU data generated it automatically figures out the correct IMU alignment and compass alignment.

Two issues:
accComputeYaw() currently uses a lookup table because I don't know the right math. I experimentally determined the correct answers for each input range.

Then at line 725 of magnetometer.js there is a TODO due to a math question.
The code currently figures out how much the compass alignment needs to be changed. But compass alignment is relative to IMU alignment, so fixing the IMU alignment will require a compensating change to the compass alignment. I don't know how to figure that out.

The file magnetometer-ai.js has a suggestion I got from Claude, but it may be completely and utterly wrong. Does it look reasonable to you?

@sensei-hacker
Copy link
Member Author

/review
--pr_reviewer.inline_code_comments=false

@sensei-hacker
Copy link
Member Author

/improve

@qodo-merge-pro
Copy link

qodo-merge-pro bot commented Nov 9, 2025

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Possible Issue

In accComputeYaw, variables are redeclared and undefined identifiers are used (e.g., reusing pitch_change/roll_change and referencing raw_changed, self without definition). Also the return expression 'return yaw + 360 % 360' likely lacks parentheses, leading to incorrect normalization.

    let pitch_change = changed[0];
    let roll_change = changed[1];

    // Normalize angles to -180 to 180 range for atan2
    if (pitch_change > 180) pitch_change -= 360;
    if (roll_change > 180) roll_change -= 360;

    // Calculate yaw based on which axis changed when nose was lifted
    let yaw = Math.atan2(-pitch_change, roll_change) * (180 / Math.PI);

    // Adjust for upside down mounting
    if (upside === 'down') {
        yaw = (yaw + 180) % 360;
    }

    // Normalize to 0-360 range
    yaw = (yaw + 360) % 360;

    // Check for 45° mounting (25.5mm boards)
    let corner_raised = false;
    let pitch_change = Math.abs(raw_changed[0]);
    let roll_change = Math.abs(raw_changed[1]);

    if (pitch_change > 20 && pitch_change < 40 &&
        roll_change > 20 && roll_change < 40 &&
        Math.abs(pitch_change - roll_change) < 15) {  
            corner_raised = true;
    }

    if (corner_raised) {
        yaw = Math.round(yaw / 22.5) * 22.5;
    } else {
        yaw = Math.round(yaw / 45) * 45;
    }

    return yaw + 360 % 360;
}
Runtime Errors

accComputeYaw function is defined twice (one in magnetometer-ai.js and another local). The local version uses nested object lookups without guarding for undefined subkeys, which can throw if corrections[upside][changed[0]] is undefined. Also comparisons like 'if (acc_g_45 < 0)' compare array to number; likely intended Z component.

function accComputeYaw(changed, upside) {
    // Is upside down just -1 X the upside up value?
    var corrections = {
        'up': {
             0: {
                45: -90, // Confirmed
                135: 0,  // Double check
                180: 0,  // Double check
                315: 90  // Confirmed
            },
            22: {
                22: -45,
                338: 45
            },
            45: {
                0: 0,   // Confirmed
                45: -45,
                90: -90,
                270: 90 // May be upside down
            },
            315: {
                0: 180, // Confirmed
                90: -90,
                270: 90
             },
            338: {
              22: -135, // aka 225
              338: 135
            }
        },
        'down': {
            0: {
              45: -90 // 90?
            },
            45: {
                0: -90 // Double check
            }
        }
    };
    if (typeof corrections[upside][changed[0]][changed[1]] != 'undefined') {
        console.log(corrections[upside][changed[0]][changed[1]]);
        return (corrections[upside][changed[0]][changed[1]]);
    } else {
        return(-1);
    }
}

function accAutoAlignReadFlat() {

    // this.heading_flat = SENSOR_DATA.kinematics[2];
    let acc_g_flat = [...SENSOR_DATA.accelerometer];

    let A = Math.sqrt(acc_g_flat[0] ** 2 + acc_g_flat[1] ** 2 + acc_g_flat[2] ** 2);

    if (A > 1.15 || A < 0.85) {
        modal = new jBox('Modal', {
            width: 460,
            height: 360,
            animation: false,
            closeOnClick: true,
            content: $('#modal-acc-align-calibration-error')
        }).open();
        return;
    }



    let roll = (Math.atan2(acc_g_flat[1], acc_g_flat[2]) * 180/Math.PI + 360) % 360;
    let pitch = ( Math.atan2(-1 * acc_g_flat[0], Math.sqrt(acc_g_flat[1] ** 2 + acc_g_flat[2] ** 2)) * 180/Math.PI +
    360) % 360;
    self.acc_flat_xyz = new Array(pitch, roll, 0);
    console.log("pitch: " + pitch + ", roll: " + roll);

    this.heading_flat = getMagHeading();

    modal = new jBox('Modal', {
        width: 460,
        height: 360,
        animation: false,
        closeOnClick: false,
        content: $('#modal-acc-align-45')
    }).open();
}



function normalizeAngle(angle) {
    return (angle + 360) % 360;
}

function accAutoAlignRead45() {
    var changed = [0, 0, 0];
    let raw_changed = [0, 0, 0];
    var acc_align;
    var i;

    let acc_g_45 = [...SENSOR_DATA.accelerometer];

    let roll = Math.atan2(acc_g_45[1], acc_g_45[2]) * 180/Math.PI;
    let pitch = Math.atan2(-1 * acc_g_45[0], Math.sqrt(acc_g_45[1] ** 2 + acc_g_45[2] ** 2)) * 180/Math.PI;
    let acc_45_xyz = new Array(pitch, roll, 0);

    for (i = 0; i < acc_g_45.length; i++) {
        self.acc_flat_xyz[i] = Math.round( self.acc_flat_xyz[i] / 45 ) * 45;
        raw_changed[i] = self.acc_flat_xyz[i] - acc_45_xyz[i];
    }
    console.log("raw_changed: " + raw_changed);


    // Check for 45° mounting (25.5mm boards)
    let corner_raised = false;
    let pitch_change = Math.abs(raw_changed[0]);
    let roll_change = Math.abs(raw_changed[1]);

    if (pitch_change > 20 && pitch_change < 40 &&
        roll_change > 20 && roll_change < 40 &&
        Math.abs(pitch_change - roll_change) < 15) {
            corner_raised = true;
    }

    for (i = 0; i < 2; i++) {
        if (corner_raised) {
            changed[i] = ( 360 + Math.round(raw_changed[i] / 22) * 22 ) % 360;
        } else {
            changed[i] = ( 360 + Math.round(raw_changed[i] / 45) * 45 ) % 360;
        }
    }

    // var boardRotation = new THREE.Euler( THREE.Math.degToRad( -self.boardAlignmentConfig.pitch ), THREE.Math.degToRad( -self.boardAlignmentConfig.yaw ), THREE.Math.degToRad( -self.boardAlignmentConfig.roll ), 'YXZ');
    // var matrix1 = (new THREE.Matrix4()).makeRotationFromEuler(boardRotation);

    /*
        Get the actual down direction.
        Get which edge or corner was lifted.
        Define front as the edge or corner that was lifted.
        Define the absolute orientation as that which has the correct part down and the correct part front.
        Apply sensor orientation on the board.
        Rotate the current settings according to the needed correction matrix.


        OR

        Get the rotation needed so that pitch and roll are near zero.
        Define front as the edge or corner that was lifted.
        Define the relative orientation (correction needed) as that which has the correct part down and the correct part front. (Relative to current settings).
        Rotate the current settings according to the needed correction matrix.

    */

  	// console.log(axis);
   	// planet.position.applyQuaternion(quaternion);

    let upside = 'up';
    if (acc_g_45 < 0) {  // Z acceleration is negative when upside down
        upside = 'down';
    }

    console.log("changed:");
    console.log(changed);
    console.log("upside: " + upside);

    let acc_yaw = accComputeYaw(changed, upside);
    self.acc_flat_xyz[2] = acc_yaw;
Logic/Math Issues

Heading/angle normalization and modulo usage are inconsistent; some paths use negative angles without normalization, and expressions like '(self.mag_saved_yaw + correction_needed) % 360' can yield negative values. Ensure consistent normalizeAngle usage and correct operator precedence in logging and modulo operations.

function accAutoAlignCompass() {
    let heading_change = (SENSOR_DATA.kinematics[2] - this.heading_flat + 360) % 360;
    // let correction_needed = (450 - SENSOR_DATA.kinematics[2]) % 360;
    let correction_needed = (90 - SENSOR_DATA.kinematics[2]);

    // let heading_change = Math.round(heading_change / 90) * 90;

    if ( typeof modal != "undefined" ) {
      modal.close();
    }

    // If a 90 degree turn caused a 270 degree change, it's upside down.
    if (heading_change > 180) {
        console.log("mag upside down");
        var rollCurrent90 = Math.round(self.mag_saved_roll / 90) * 90;
        // let newRoll = (rollCurrent90 - 180) % 360;
        updateRollAxis( (rollCurrent90 - 180) % 360 );
        // ? correction_needed = correction_needed + 180;
    }
    // If both headings are accurate along a 45° offset, use that. Otherwise round to nearest 90°
    if ( (Math.abs(this.heading_flat % 45) < 15) && Math.abs(correction_needed % 45) < 15 ) {
        correction_needed = ( Math.round(correction_needed / 45) * 45 ) % 360;
    } else {
        correction_needed = ( Math.round(correction_needed / 90) * 90 ) % 360;
    }

    console.log("heading_flat: " + this.heading_flat + ", change: " + heading_change + ", correction: " + correction_needed % 360);
    // let newYaw = (self.mag_saved_yaw + correction_needed) % 360;
    updateYawAxis( (self.mag_saved_yaw + correction_needed) % 360 );
    updatePitchAxis(self.mag_saved_pitch);

    $("#modal-compass-align-setting").text(
            self.alignmentConfig.roll + ", " + self.mag_saved_pitch + ", " + self.alignmentConfig.yaw
    );
    modal = new jBox('Modal', {
        width: 460,
        height: 360,
        animation: false,
        closeOnClick: true,
        content: $('#modal-acc-align-done')
    }).open();
}

@qodo-merge-pro
Copy link

qodo-merge-pro bot commented Nov 9, 2025

PR Code Suggestions ✨

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix variable shadowing and undefined reference

Fix a syntax error and a reference error by renaming the redeclared pitch_change
and roll_change variables and correcting the use of the undefined raw_changed
variable.

tabs/magnetometer-ai.js [6-27]

 let pitch_change = changed[0];
 let roll_change = changed[1];
 
 // Normalize angles to -180 to 180 range for atan2
 if (pitch_change > 180) pitch_change -= 360;
 if (roll_change > 180) roll_change -= 360;
 
 // Calculate yaw based on which axis changed when nose was lifted
 let yaw = Math.atan2(-pitch_change, roll_change) * (180 / Math.PI);
 
 // Adjust for upside down mounting
 if (upside === 'down') {
     yaw = (yaw + 180) % 360;
 }
 
 // Normalize to 0-360 range
 yaw = (yaw + 360) % 360;
 
 // Check for 45° mounting (25.5mm boards)
 let corner_raised = false;
-let pitch_change = Math.abs(raw_changed[0]);
-let roll_change = Math.abs(raw_changed[1]);
+const abs_pitch_change = Math.abs(changed[0]);
+const abs_roll_change = Math.abs(changed[1]);
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a syntax error due to variable redeclaration and a reference error from using an undefined variable raw_changed, both of which would crash the function.

High
Use correct context and normalize angles

Fix an undefined reference by using self.heading_flat instead of
this.heading_flat and ensure correction_needed is properly normalized to avoid
negative angles.

tabs/magnetometer.js [805-808]

 function accAutoAlignCompass() {
-    let heading_change = (SENSOR_DATA.kinematics[2] - this.heading_flat + 360) % 360;
-    // let correction_needed = (450 - SENSOR_DATA.kinematics[2]) % 360;
-    let correction_needed = (90 - SENSOR_DATA.kinematics[2]);
+    const headingFlat = self.heading_flat;
+    let heading_change = (SENSOR_DATA.kinematics[2] - headingFlat + 360) % 360;
+    let correction_needed = (90 - SENSOR_DATA.kinematics[2] + 360) % 360;
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that this.heading_flat will be undefined due to incorrect context (this) and also points out a potential issue with negative angles, both of which are critical bugs.

High
Correct angle normalization precedence

Correct the angle normalization logic by adding parentheses to ensure the modulo
operation is applied correctly.

tabs/magnetometer-ai.js [35-41]

 if (corner_raised) {
     yaw = Math.round(yaw / 22.5) * 22.5;
 } else {
     yaw = Math.round(yaw / 45) * 45;
 }
 
-return yaw + 360 % 360;
+return (yaw + 360) % 360;
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies a bug in the angle normalization logic due to incorrect operator precedence, which would cause the function to return incorrect values.

Medium
General
Fix malformed translation content

Remove a malformed and redundant div element from the modal content to fix a UI
rendering issue.

tabs/magnetometer.html [52-59]

 <div class="modal__content">
     <h1 class="modal__title" data-i18n="accAlignFinished"></h1>
     <div class="modal__text">
-        <div data-i18n="accAlignFinished">accAlignFinished></div>
         Board alignment set to <span id="modal-acc-align-setting"></span><br />
         Compass alignment set to <span id="modal-compass-align-setting"></span>
     </div>
 </div>
  • Apply / Chat
Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies and fixes malformed HTML content (accAlignFinished>) which would cause incorrect rendering in the UI.

Low
  • More

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Don't merge Enhancement Enhancement of existing functions Review effort 4/5

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant