
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:
- Alert Indicators , Displays warnings and critical statuses.
- Status Indicators, for flight status.
- Segment LED Displays: Show key data.
- LCD Display Module: Provides additional game-related information.
- Analog displays
- Input buttons for various flight-related commands and indicators.
- 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
invertedis0:- A green alert is triggered if
resourceis greater than or equal tothreshold_Y. - A yellow alert is triggered if
resourceis betweenthreshold_Randthreshold_Y. - A red alert is triggered if
resourceis less thanthreshold_R.
- A green alert is triggered if
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
torqueMeasureto 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
targetAngleand thecurrentAngleto calculate the desired angular velocity (targetW). - Adjusts
targetWbased on speed constraints, such asspeedLimit_rotationandspeedAdjust_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.
- Retrieves the current angular velocity (
- Output:
- Returns a floating-point value representing the torque adjustment required to move the system toward the
targetAngle.
- Returns a floating-point value representing the torque adjustment required to move the system toward the
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
}
}