Flight Controller Design KSP

I built an Arduino-powered flight controller that integrates with the Kerbal Space Program (KSP) game using the Kerbal Simpit API. The hardware communicates with the game by loading data in each cycle and sending user inputs back for control.

Features:

  1. Alert Indicators , Displays warnings and critical statuses.
  2. Status Indicators, for flight status.
  3. Segment LED Displays: Show key data.
  4. LCD Display Module: Provides additional game-related information.
  5. Analog displays
  6. Input buttons for various flight-related commands and indicators.
  7. Rotary Switchs: Used for controlling the LCD menus.

Hardware:

  • Microcontroller: Controlled with an Arduino Mega for increased input and output capacity.
  • Expanders: Utilizes 74HC595 Shift Registers and PCF8574 Port Expanders to handle additional inputs and outputs.
  • Modular Design: All parts are 3D-printed and designed for easy assembly and replacement.

SOFTWARE

Allert Modules

Parameters:

  • resource: The current value of the resource being monitored.
  • alertId: An identifier for the alert.
  • threshold_Y: The threshold value for triggering a yellow alert.
  • threshold_R: The threshold value for triggering a red alert.
  • inverted: A flag indicating if the alert logic should be inverted (0 for normal, 1 for inverted).

Logic Flow:

  • If inverted is 0:
    • A green alert is triggered if resource is greater than or equal to threshold_Y.
    • A yellow alert is triggered if resource is between threshold_R and threshold_Y.
    • A red alert is triggered if resource is less than threshold_R.
void alertSelect(float currentResourceLevel, int alertId, float yellowThreshold, float redThreshold, int isInverted) {
    // Check for valid inverted flag
    if (isInverted != 0 && isInverted != 1) {
        // Handle invalid inverted value, e.g., log an error or set a default behavior
        return;
    }

    // Determine if the alert logic should be inverted
    bool inverted = (isInverted == 1);

    // Set alert levels based on resource level and thresholds
    if ((inverted && currentResourceLevel <= yellowThreshold) || (!inverted && currentResourceLevel >= yellowThreshold)) {
        alertModule("alert", alertId, 1); // Green alert
    } else if ((inverted && currentResourceLevel <= redThreshold && currentResourceLevel > yellowThreshold) ||
               (!inverted && currentResourceLevel >= redThreshold && currentResourceLevel < yellowThreshold)) {
        alertModule("alert", alertId, 3); // Yellow alert
    } else if ((inverted && currentResourceLevel > redThreshold) || (!inverted && currentResourceLevel < redThreshold)) {
        alertModule("alert", alertId, 2); // Red alert
    }
}

Flight Assistant

The flightAssistant function assists in flight control by calculating and applying the required torque adjustments for roll, pitch, and heading to guide an aircraft . This function integrates user-set targets, from LCD menu.

Functionality

  • Input: Reads target values for heading, pitch, and roll from the user interface (menuArray_values).
  • Torque Calculation: Uses torqueMeasure to calculate the required torque for each axis, ensuring precise adjustments.
  • Coordinate Adjustment: Transforms the torque values to account for the roll angle (alpha) and align them with the aircraft’s global frame of reference.
  • Output: Packages and sends the torque commands to the flight controller as a rotationMessage.

const float DEGREE_TO_RADIAN = 0.01745;
const float TORQUE_MAG = 1000.0;

// Flight Assistant Function
void flightAssistant() {

    // Retrieve target angles
    int target_heading = menuArray_values[0][1];
    int target_pitch = menuArray_values[0][2];
    int target_roll = menuArray_values[0][3];

    // Initialize torques
    float torque_roll = 0.0;
    float torque_pitch = 0.0;
    float torque_heading = 0.0;

    // Calculate torques based on states
    if (!roll_state) {
        torque_roll = torqueMeasure("roll", roll, target_roll);
    }
    if (!pitch_state) {
        torque_pitch = torqueMeasure("pitch", pitch, target_pitch);
    }
    if (!heading_state) {
        torque_heading = torqueMeasure("heading", heading, target_heading);
    }

    // Apply transformation for heading and pitch based on roll
    float alpha = roll * DEGREE_TO_RADIAN; // Convert roll to radians
    float adjusted_heading = (torque_heading * cos(alpha)) + (torque_pitch * sin(alpha));
    float adjusted_pitch = (-torque_heading * sin(alpha)) + (torque_pitch * cos(alpha));

    // Prepare rotation message
    rotationMessage rot_msg;
    rot_msg.setRoll(-TORQUE_MAG * torque_roll);
    rot_msg.setPitch(TORQUE_MAG * adjusted_pitch);
    rot_msg.setYaw(TORQUE_MAG * adjusted_heading);

    // Send rotation message
    mySimpit.send(ROTATION_MESSAGE, rot_msg);
}

Torque Measure: function calculates the required torque adjustment for a specified axis ( roll, pitch, or heading) to align a current angular position with a target angle.

  • Input Parameters:
    • dir: Specifies the axis of rotation (“roll”, “pitch”, or “heading”).
    • currentAngle: The current angular position of the system on the specified axis.
    • targetAngle: The desired angular position to achieve.
  • Internal Processing:
    • Retrieves the current angular velocity (angularW) for the specified axis.
    • Computes the difference between the targetAngle and the currentAngle to calculate the desired angular velocity (targetW).
    • Adjusts targetW based on speed constraints, such as speedLimit_rotation and speedAdjust_angle, using a constraining function.
    • Calculates the torque as the difference between the desired angular velocity (targetW) and the actual angular velocity (angularW).
    • If the angular difference is very small and the system has reached the target, sets the torque to 0.
  • Output:
    • Returns a floating-point value representing the torque adjustment required to move the system toward the targetAngle.
float torqueMeasure(String dir, float currentAngle, float targetAngle) {
    float angularW;

    // Map direction to the corresponding angular velocity
    if (dir == "roll") angularW = angularW_roll;
    else if (dir == "heading") angularW = angularW_heading;
    else if (dir == "pitch") angularW = angularW_pitch;
    else return 0; // Return zero if direction is invalid

    const float speedLimit_rotation = 10; // Maximum rotational speed
    const float speedAdjust_angle = 20;  // Angle adjustment factor

    // Calculate target angular velocity
    float angleDifference = targetAngle - currentAngle;
    float targetW = constrain(angleDifference, -speedAdjust_angle, speedAdjust_angle);
    targetW *= speedLimit_rotation / speedAdjust_angle;

    // Determine torque based on angle difference
    float torque = (abs(angleDifference) <= speedAdjust_angle) 
                   ? targetW - angularW
                   : (angleDifference > 0 ? speedLimit_rotation : -speedLimit_rotation);

    // Zero out torque if target reached
    if (abs(targetW) < 0.1 && targetAngle == currentAngle) torque = 0;

    return torque;
}

LCD Menu

The lcd_updateMenu function updates the LCD display to reflect the current menu state. The rotaryUpdate_A and rotaryUpdate_B functions handle input from rotary encoders, allowing users to navigate through the menu and adjust settings by rotating the encoder or pressing its button.


MenuArray_values: Stores menu item labels & values.

  String menuArray[5][4] = 
  { 
    { "ROTATION_ABS","HEAD", "PITC", "ROLL"},
    { "ROTATION_REL","HEAD", "PITC", "ROLL"},  
    { "CTRL_LIMIT  ","ROTW ", "DIR ", "ROLL"},  
    { "SPEED_CTRL  ","HOR ", "VERT", "SURF"},
    { "MENU_E      ","E1", "E2", "E3"},
  };
  int menuArray_values[5][4] = 
  { 
    { 0,89,0,0},
    { 0,0,0,0},
    { 0,30,0,0},
    { 0,0,0,0},
    { 0,0,0,0},
  };

lcd_updateMenu is responsible for updating the LCD display with the current menu information. Updates the LCD screen with the current menu and values. Clears the LCD and prints the menu title and values when lcdContentUpdate is true.

void lcd_updateMenu() {
  // Check if the LCD content needs to be updated
  if (lcdContentUpdate) {
    lcd.clear(); // Clear the LCD screen

    // Display the menu title
    lcd.setCursor(0, 0); // Set cursor to the beginning of the first line
    lcd.print("MAIN " + menuArray[subMenu_ID][0]); // Print the main menu title

    // Display the menu items and their values
    for (int i = 1; i < 4; i++) {
      lcd.setCursor(0, i); // Set cursor to the beginning of each line
      // Print the menu item with a selection indicator if it's the current item
      lcd.print((i == item_ID ? ">" : " ") + menuArray[subMenu_ID][i] + ": ");
      lcd.print(menuArray_values[subMenu_ID][i]); // Print the current value of the menu item
    }

    lcdContentUpdate = false; // Reset the update flag
  }
}


rotaryUpdate_A function handles input from the first rotary encoder and updates the menu selection accordingly:Updates item_ID and counter_A based on the rotary encoder’s movement. Sets lcdContentUpdate to true if the encoder is rotated or the button is pressed.

void rotaryUpdate_A() {
  // Read the current state of the rotary encoder pins
  int currentStateRotary_CLK = PCF1.read(rotary_CLK_A);
  int currentStateRotary_DT = PCF1.read(rotary_DT_A);
  int currentStateRotary_SW = PCF1.read(rotary_SW_A);

  // Check if the rotary encoder was turned
  if (currentStateRotary_CLK != lastStateRotary_CLK_A && currentStateRotary_CLK == HIGH) {
    // Determine the direction of rotation
    if (currentStateRotary_DT != currentStateRotary_CLK) {
      counter_A--; // Counter decreases for one direction
    } else {
      counter_A++; // Counter increases for the other direction
    }
    item_ID = abs(counter_A % 4); // Update the selected menu item
    lcdContentUpdate = true; // Set the flag to update the LCD content
  }

  lastStateRotary_CLK_A = currentStateRotary_CLK; // Update the last state

  // Check if the rotary encoder button was pressed
  if (currentStateRotary_SW == LOW && (millis() - lastButtonPress_A > 50)) {
    lcdContentUpdate = true; // Set the flag to update the LCD content
    lastButtonPress_A = millis(); // Update the last button press time
  }
}


rotaryUpdate_B: handles input from the second rotary encoder and updates menu values or submenu selection accordingly: Adjusts subMenu_ID or updates the value of menuArray_values based on the encoder’s position and movement. Uses a secondary variable increment to change the step size (1 or 10) based on button presses.

void rotaryUpdate_B() {
  int delta = 0; // Initialize the delta for changes
  // Read the current state of the rotary encoder pins
  int currentStateRotary_CLK = PCF1.read(rotary_CLK_B);
  int currentStateRotary_DT = PCF1.read(rotary_DT_B);
  int currentStateRotary_SW = PCF1.read(rotary_SW_B);

  // Check if the rotary encoder was turned
  if (currentStateRotary_CLK != lastStateRotary_CLK_B && currentStateRotary_CLK == HIGH) {
    lcdContentUpdate = true; // Set the flag to update the LCD content
    delta = (currentStateRotary_DT != currentStateRotary_CLK) ? -1 : 1; // Determine the direction of rotation

    if (item_ID == 0) {
      // Update the submenu selection
      subMenu_ID = constrain(subMenu_ID + delta, 0, subMenu_NUM - 1);
    } else {
      // Update the value of the selected menu item
      menuArray_values[subMenu_ID][item_ID] += delta * increment;
    }
  }

  lastStateRotary_CLK_B = currentStateRotary_CLK; // Update the last state

  // Check if the rotary encoder button was pressed
  if (currentStateRotary_SW == LOW && (millis() - lastButtonPress_B > 50)) {
    increment = (increment == 1) ? 10 : 1; // Toggle the increment value
    lastButtonPress_B = millis(); // Update the last button press time
  }
}


References:

Leave a comment