works
This commit is contained in:
novatic14
2026-03-11 15:43:47 -07:00
parent 7dd1170b71
commit cb5940f54e
14 changed files with 1965 additions and 0 deletions

Binary file not shown.

Binary file not shown.

BIN
CAD Files/Rocket Nozzle.f3z Normal file

Binary file not shown.

View File

@@ -0,0 +1,150 @@
/**
* ESP32 Rocket Fin Calibrator (FIXED)
* * MAPPING:
* A -> Servo 27 (UP)
* B -> Servo 14 (DOWN)
* C -> Servo 26 (RIGHT)
* D -> Servo 25 (LEFT)
*/
#include <Arduino.h>
#include <ESP32Servo.h>
// -----------------------------------------------------------
// CONFIGURATION
// -----------------------------------------------------------
const int NUM_SERVOS = 4;
// Pin Definitions
const int PIN_UP = 27;
const int PIN_DOWN = 14;
const int PIN_RIGHT = 26;
const int PIN_LEFT = 25;
// Arrays for easy indexing: 0=UP, 1=DOWN, 2=RIGHT, 3=LEFT
int servoPins[NUM_SERVOS] = {PIN_UP, PIN_DOWN, PIN_RIGHT, PIN_LEFT};
String servoNames[NUM_SERVOS] = {"UP (27)", "DOWN (14)", "RIGHT (26)", "LEFT (25)"};
// This array holds the live center angle for each servo
int centerAngles[NUM_SERVOS] = {90, 90, 90, 90};
// Track which servo is currently selected (Default to 0 / UP)
int activeIndex = 0;
Servo servos[NUM_SERVOS];
// -----------------------------------------------------------
// FUNCTION PROTOTYPES (This fixes the error!)
// -----------------------------------------------------------
void printInstructions();
void printSelection();
void changeAngle(int amount);
// -----------------------------------------------------------
// SETUP
// -----------------------------------------------------------
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
// Allocate timers
ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
ESP32PWM::allocateTimer(2);
ESP32PWM::allocateTimer(3);
Serial.println("--- Rocket Fin Calibrator Started ---");
// Attach all servos and move to initial 90
for (int i = 0; i < NUM_SERVOS; i++) {
servos[i].setPeriodHertz(50);
servos[i].attach(servoPins[i]);
servos[i].write(centerAngles[i]);
}
printInstructions();
}
// -----------------------------------------------------------
// LOOP
// -----------------------------------------------------------
void loop() {
if (Serial.available() > 0) {
char cmd = Serial.read();
// Ignore newline/carriage return characters
if (cmd == '\n' || cmd == '\r') return;
// Convert lowercase a,b,c,d to uppercase
cmd = toupper(cmd);
// --- SELECTION LOGIC ---
if (cmd == 'A') {
activeIndex = 0;
printSelection();
}
else if (cmd == 'B') {
activeIndex = 1;
printSelection();
}
else if (cmd == 'C') {
activeIndex = 2;
printSelection();
}
else if (cmd == 'D') {
activeIndex = 3;
printSelection();
}
// --- ADJUSTMENT LOGIC ---
else if (cmd == '+') {
changeAngle(1);
}
else if (cmd == '-') {
changeAngle(-1);
}
}
}
// -----------------------------------------------------------
// HELPER FUNCTIONS
// -----------------------------------------------------------
void changeAngle(int amount) {
// Update the angle for the CURRENTLY selected servo only
centerAngles[activeIndex] += amount;
// Safety limits (0 to 180)
if (centerAngles[activeIndex] > 180) centerAngles[activeIndex] = 180;
if (centerAngles[activeIndex] < 0) centerAngles[activeIndex] = 0;
// Move the servo
servos[activeIndex].write(centerAngles[activeIndex]);
// Print Update
Serial.print(">>> Adjusted ");
Serial.print(servoNames[activeIndex]);
Serial.print(" | New Center: ");
Serial.println(centerAngles[activeIndex]);
}
void printSelection() {
Serial.print("\n>>> SELECTED: ");
Serial.println(servoNames[activeIndex]);
Serial.print(" Current Angle: ");
Serial.println(centerAngles[activeIndex]);
Serial.println(" Use '+' or '-' to adjust.");
}
void printInstructions() {
Serial.println("----------------------------------------");
Serial.println("Type 'A' -> Select UP (27)");
Serial.println("Type 'B' -> Select DOWN (14)");
Serial.println("Type 'C' -> Select RIGHT (26)");
Serial.println("Type 'D' -> Select LEFT (25)");
Serial.println("----------------------------------------");
Serial.println("Then use '+' or '-' to adjust that servo.");
Serial.println("----------------------------------------");
// Select A by default on startup
printSelection();
}

View File

@@ -0,0 +1,126 @@
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BMP085.h>
#include <QMC5883LCompass.h>
// Initialize sensor objects
Adafruit_BMP085 bmp;
QMC5883LCompass compass;
// Flags and variables
bool bmp_working = false;
const int I2C_SDA = 21;
const int I2C_SCL = 22;
// Function to scan the bus and return number of devices
int scanI2CBus() {
Serial.println("--- Scanning I2C Bus ---");
byte error, address;
int nDevices = 0;
// A standard I2C scanner helps determine if the hardware is electrically connected
for(address = 1; address < 127; address++ ) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.print("Found device at 0x");
if (address < 16) Serial.print("0");
Serial.print(address, HEX);
// Identify common addresses
if (address == 0x77) Serial.println(" (Likely BMP180)");
else if (address == 0x0D) Serial.println(" (Likely QMC5883L)");
else Serial.println(" (Unknown Device)");
nDevices++;
} else if (error == 4) {
Serial.print("Unknown error at address 0x");
if (address < 16) Serial.print("0");
Serial.println(address, HEX);
}
}
return nDevices;
}
void setup() {
Serial.begin(115200);
delay(2000); // Give serial monitor time to connect
// Initialize I2C with stability settings
// Default for ESP32 DevKit V1 is SDA=21, SCL=22
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000); // Standard 100kHz mode is safer for jumper wires
Wire.setTimeOut(1000); // Timeout to prevent the CPU from hanging on a bad bus
int devicesFound = scanI2CBus();
if (devicesFound == 0) {
Serial.println("ERROR: No I2C devices detected!");
Serial.println("1. Check if 3.3V and GND are connected to both sensors.");
Serial.println("2. Check if SDA is on Pin 21 and SCL is on Pin 22.");
Serial.println("3. Ensure your soldering joints are making contact with the pads.");
}
Serial.println("\n--- Initializing Sensor Libraries ---");
// BMP180 Initialization
if (!bmp.begin()) {
Serial.println("BMP180: FAILED to initialize library.");
bmp_working = false;
} else {
Serial.println("BMP180: SUCCESS");
bmp_working = true;
}
// QMC5883L Initialization
compass.init();
compass.setSmoothing(10, true);
Serial.println("QMC5883L: Initialized (Note: init() does not check for presence)");
Serial.println("----------------------------\n");
}
void loop() {
Serial.println("========== SENSOR DATA ==========");
// 1. Process BMP180
if (bmp_working) {
float temp = bmp.readTemperature();
// Check for logical errors (extreme values)
if (temp < -40 || temp > 80) {
Serial.println("[BMP180] Data Error: Reading out of range.");
} else {
Serial.print("[BMP180] Temp: "); Serial.print(temp);
Serial.print(" C | Pressure: "); Serial.print(bmp.readPressure()); Serial.println(" Pa");
}
} else {
Serial.println("[BMP180] OFFLINE (Initialization failed)");
}
// 2. Process QMC5883L
compass.read();
int x = compass.getX();
int y = compass.getY();
int z = compass.getZ();
// If the compass returns all zeros, it is physically disconnected or the bus crashed
if (x == 0 && y == 0 && z == 0) {
Serial.println("[QMC5883L] OFFLINE (Returning all zeros)");
// Attempt emergency bus recovery if we lost communication
Serial.println("Attempting I2C Bus Reset...");
Wire.end();
delay(100);
Wire.begin(I2C_SDA, I2C_SCL);
Wire.setClock(100000);
} else {
Serial.print("[QMC5883L] X: "); Serial.print(x);
Serial.print(" | Y: "); Serial.print(y);
Serial.print(" | Z: "); Serial.print(z);
Serial.print(" | Heading: "); Serial.print(compass.getAzimuth()); Serial.println("°");
}
Serial.println("=================================\n");
delay(1000);
}

View File

@@ -0,0 +1,339 @@
/* =========================================================================
* MPU6050 ROCKET STABILIZATION - ACCESS POINT (AP) VERSION
* =========================================================================
* CHANGES FROM PREVIOUS:
* 1. WiFi Mode: Creates its own WiFi network (SoftAP)
* 2. Addressing: Defaults to BROADCAST IP until it hears from a specific computer
* 3. Discovery: Auto-detects the computer's IP when it receives a command
* 4. PID Tuning: Added runtime PID tuning and saving to internal Flash (NVS)
* ========================================================================= */
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <ESP32Servo.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Preferences.h>
// --- WiFi / Network Configuration ---
// The Rocket will CREATE this network
const char* ssid = "RocketLink_Telemetry";
const char* password = "rocketlaunch";
// Telemetry Destination
// Initially set to BROADCAST so any connected computer can hear it immediately.
// Once a command is received, this updates to the specific sender's IP.
IPAddress remoteIp(192, 168, 4, 255);
const int remotePort = 4444; // Port to send telemetry TO
const int localPort = 4444; // Port to listen for commands ON
WiFiUDP udp;
Preferences preferences;
// --- Hardware Pin Definitions ---
const int LEFT_SERVO_PIN = 26;
const int RIGHT_SERVO_PIN = 25;
const int UP_SERVO_PIN = 27;
const int DOWN_SERVO_PIN = 14;
// --- Servo Settings ---
Servo leftServo;
Servo rightServo;
Servo upServo;
Servo downServo;
const int LEFT_CENTER = 115;
const int RIGHT_CENTER = 80;
const int UP_CENTER = 80;
const int DOWN_CENTER = 115;
const int MAX_DEFLECTION = 15;
// --- PID & Stabilization ---
Adafruit_MPU6050 mpu;
float Kp = 0.5; // Default, overridden by preferences in setup
float Kd = 0.2; // Default, overridden by preferences in setup
float roll = 0;
float gyroX_offset = 0;
// --- State Machine ---
enum RocketState {
STATE_IDLE,
STATE_LAUNCHING,
STATE_FLIGHT
};
RocketState currentState = STATE_IDLE;
// --- Launch Detection Constants ---
const float LAUNCH_THRESHOLD_G = 1.6;
const float G_TO_MS2 = 9.80665;
const float LAUNCH_THRESHOLD_MS2 = LAUNCH_THRESHOLD_G * G_TO_MS2;
const int LAUNCH_DURATION_MS = 50;
unsigned long launchTriggerStartTime = 0;
// --- Timing Variables ---
unsigned long last_time;
float dt;
unsigned long lastStateMsgTime = 0;
const int STATE_MSG_INTERVAL = 2000;
// --- Function Prototypes ---
void setServosEnabled(bool enabled);
void changeState(RocketState newState);
void sendTelemetry(float roll, float rate, int out);
void sendStateUpdate();
void calibrateGyro();
void setup(void) {
Serial.begin(115200);
Wire.begin(21, 22);
// 1. Setup Servos
ESP32PWM::allocateTimer(0);
ESP32PWM::allocateTimer(1);
ESP32PWM::allocateTimer(2);
ESP32PWM::allocateTimer(3);
leftServo.setPeriodHertz(50);
rightServo.setPeriodHertz(50);
upServo.setPeriodHertz(50);
downServo.setPeriodHertz(50);
setServosEnabled(false);
// 2. Setup MPU6050
if (!mpu.begin()) {
Serial.println("Failed to find MPU6050 chip");
while (1) { delay(10); }
}
Serial.println("MPU6050 Found!");
mpu.setAccelerometerRange(MPU6050_RANGE_16_G);
mpu.setGyroRange(MPU6050_RANGE_500_DEG);
mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);
// 3. Load Calibration and PID config
preferences.begin("rocket_cfg", false);
gyroX_offset = preferences.getFloat("gyro_bias", 0.0);
Kp = preferences.getFloat("Kp", 0.5); // Load saved Kp, default to 0.5
Kd = preferences.getFloat("Kd", 0.2); // Load saved Kd, default to 0.2
Serial.println("--- Saved Config Loaded ---");
Serial.print("Gyro Bias: "); Serial.println(gyroX_offset);
Serial.print("Kp: "); Serial.println(Kp);
Serial.print("Kd: "); Serial.println(Kd);
Serial.println("---------------------------");
// 4. Setup Access Point (AP)
Serial.println("Starting Access Point...");
WiFi.softAP(ssid, password);
// The default IP for ESP32 SoftAP is usually 192.168.4.1
Serial.print("AP Created. Connect to: "); Serial.println(ssid);
Serial.print("Rocket IP: "); Serial.println(WiFi.softAPIP());
udp.begin(localPort);
Serial.println("System Ready. Broadcasting Telemetry...");
last_time = millis();
}
void loop() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
// Calculate Delta Time
unsigned long current_time = millis();
dt = (current_time - last_time) / 1000.0;
last_time = current_time;
if (dt <= 0) return;
// --- Network: Receive Commands & Auto-Detect Remote IP ---
int packetSize = udp.parsePacket();
if (packetSize) {
// CAPTURE SENDER IP: This enables 2-way comms without hardcoding
IPAddress senderIp = udp.remoteIP();
if (remoteIp != senderIp) {
remoteIp = senderIp;
Serial.print("Connected to Ground Station at: ");
Serial.println(remoteIp);
}
char packetBuffer[255];
int len = udp.read(packetBuffer, 255);
if (len > 0) packetBuffer[len] = 0;
String command = String(packetBuffer);
command.trim();
// -- Parse Tuning Commands (Any state) --
if (command.startsWith("PID,")) {
// Expected format: PID,0.5,0.2
int firstComma = command.indexOf(',');
int secondComma = command.indexOf(',', firstComma + 1);
if (firstComma > 0 && secondComma > 0) {
Kp = command.substring(firstComma + 1, secondComma).toFloat();
Kd = command.substring(secondComma + 1).toFloat();
// Save to internal flash so it survives reboots
preferences.putFloat("Kp", Kp);
preferences.putFloat("Kd", Kd);
Serial.print("New PID Saved -> Kp: "); Serial.print(Kp);
Serial.print(" Kd: "); Serial.println(Kd);
// Send updated state immediately so dashboard syncs
sendStateUpdate();
}
}
// -- Parse Mission Commands (Only in IDLE) --
else if (currentState == STATE_IDLE) {
if (command.equalsIgnoreCase("launch")) {
changeState(STATE_LAUNCHING);
}
else if (command.equalsIgnoreCase("calibrate")) {
Serial.println("Recalibration Command Received.");
calibrateGyro();
roll = 0;
}
}
}
// --- Network: Send State Heartbeat ---
if (millis() - lastStateMsgTime > STATE_MSG_INTERVAL) {
sendStateUpdate();
lastStateMsgTime = millis();
}
// --- Gyro Integration ---
float raw_rate_rad = g.gyro.x - gyroX_offset;
float rate_deg_s = raw_rate_rad * 180.0 / PI;
roll += rate_deg_s * dt;
int currentServoOffset = 0;
// --- STATE MACHINE LOGIC ---
switch (currentState) {
case STATE_IDLE:
break;
case STATE_LAUNCHING:
{
float accel_mag = sqrt(sq(a.acceleration.x) + sq(a.acceleration.y) + sq(a.acceleration.z));
if (accel_mag > LAUNCH_THRESHOLD_MS2) {
if (launchTriggerStartTime == 0) {
launchTriggerStartTime = millis();
} else if (millis() - launchTriggerStartTime > LAUNCH_DURATION_MS) {
changeState(STATE_FLIGHT);
}
} else {
launchTriggerStartTime = 0;
}
}
break;
case STATE_FLIGHT:
{
float P_term = Kp * roll;
float D_term = Kd * rate_deg_s;
float output = P_term + D_term;
currentServoOffset = (int)output;
currentServoOffset = constrain(currentServoOffset, -MAX_DEFLECTION, MAX_DEFLECTION);
leftServo.write(LEFT_CENTER + currentServoOffset);
rightServo.write(RIGHT_CENTER + currentServoOffset);
upServo.write(UP_CENTER + currentServoOffset);
downServo.write(DOWN_CENTER + currentServoOffset);
}
break;
}
// --- TELEMETRY ---
sendTelemetry(roll, rate_deg_s, currentServoOffset);
delay(5);
}
// --- Helper Functions ---
void calibrateGyro() {
Serial.println("Calibrating Gyro... KEEP STILL");
delay(1000);
float sumX = 0;
int calibration_samples = 200;
for (int i = 0; i < calibration_samples; i++) {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
sumX += g.gyro.x;
delay(5);
}
gyroX_offset = sumX / calibration_samples;
preferences.putFloat("gyro_bias", gyroX_offset);
Serial.print("New Gyro Offset Saved: "); Serial.println(gyroX_offset);
}
void changeState(RocketState newState) {
currentState = newState;
switch (currentState) {
case STATE_IDLE:
setServosEnabled(false);
break;
case STATE_LAUNCHING:
setServosEnabled(false);
launchTriggerStartTime = 0;
roll = 0;
break;
case STATE_FLIGHT:
setServosEnabled(true);
break;
}
sendStateUpdate();
lastStateMsgTime = millis();
}
void setServosEnabled(bool enabled) {
if (enabled) {
if (!leftServo.attached()) leftServo.attach(LEFT_SERVO_PIN, 500, 2400);
if (!rightServo.attached()) rightServo.attach(RIGHT_SERVO_PIN, 500, 2400);
if (!upServo.attached()) upServo.attach(UP_SERVO_PIN, 500, 2400);
if (!downServo.attached()) downServo.attach(DOWN_SERVO_PIN, 500, 2400);
} else {
if (leftServo.attached()) leftServo.detach();
if (rightServo.attached()) rightServo.detach();
if (upServo.attached()) upServo.detach();
if (downServo.attached()) downServo.detach();
}
}
void sendStateUpdate() {
// Send to the current remoteIp (Starts as broadcast .255, then becomes specific)
// Modified to include Kp and Kd so dashboard stays in sync
udp.beginPacket(remoteIp, remotePort);
udp.print("STATUS:");
switch (currentState) {
case STATE_IDLE: udp.print("IDLE"); break;
case STATE_LAUNCHING: udp.print("LAUNCHING"); break;
case STATE_FLIGHT: udp.print("FLIGHT"); break;
}
udp.print(",");
udp.print(Kp);
udp.print(",");
udp.print(Kd);
udp.endPacket();
}
void sendTelemetry(float roll, float rate, int out) {
udp.beginPacket(remoteIp, remotePort);
udp.print("T,");
udp.print(millis());
udp.print(",");
udp.print(roll, 1);
udp.print(",");
udp.print(rate, 1);
udp.print(",");
udp.print(out);
udp.endPacket();
}

View File

@@ -0,0 +1,411 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import socket
import threading
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from collections import deque
import time
import numpy as np
# --- Configuration ---
UDP_IP = "0.0.0.0"
UDP_PORT = 4444
BUFFER_SIZE = 1024
VIEW_POINTS = 100
RATE_SCALE = 0.25
LAUNCHER_GATEWAY = "192.168.4.1"
class TelemetryApp:
def __init__(self, root):
self.root = root
self.root.title("Rocket Telemetry Dashboard - Ground Control")
self.root.geometry("1100x850")
self.zoom_levels = [1.0, 1.5, 2.0, 2.5]
self.ui_scale = 1.0
self.time_data = deque()
self.roll_data = deque()
self.rate_data = deque()
self.output_data = deque()
self.current_values = {
"Time": 0, "Roll": 0.0, "Rate": 0.0, "Output": 0.0,
"State": "DISCONNECTED", "ActiveKp": 0.0, "ActiveKd": 0.0,
"Skew": 0.0,
"Lat": 0.0, "Lon": 0.0, "Alt": 0.0, "GPS_State": 0
}
self.mission_events = []
self.last_state = "DISCONNECTED"
self.kp_var = tk.StringVar(value="0.5")
self.kd_var = tk.StringVar(value="0.2")
self.rocket_ip = None
self.running = True
self.build_gui()
self.listener_thread = threading.Thread(target=self.udp_listener, daemon=True)
self.listener_thread.start()
self.watchdog_thread = threading.Thread(target=self.connection_watchdog, daemon=True)
self.watchdog_thread.start()
self.gui_update_loop()
def s(self, val): return int(val * self.ui_scale)
def f(self, size, weight="normal"): return ("Helvetica", self.s(size), weight)
def fm(self, size): return ("Courier New", self.s(size), "bold")
def build_gui(self):
style = ttk.Style()
style.configure('TButton', font=('Helvetica', self.s(10)))
style.configure('TLabelframe.Label', font=('Helvetica', self.s(10), 'bold'))
for widget in self.root.winfo_children(): widget.destroy()
if hasattr(self, 'anim') and self.anim and self.anim.event_source:
self.anim.event_source.stop()
self.setup_ui()
self.setup_plot()
def update_scale_val(self, val): pass
def trigger_redraw(self, event):
val = self.scale_slider.get()
idx = int(round(val))
self.scale_slider.set(idx)
self.ui_scale = self.zoom_levels[idx]
self.build_gui()
def setup_ui(self):
control_frame = ttk.Frame(self.root, padding=self.s(10))
control_frame.pack(side=tk.TOP, fill=tk.X)
ttk.Label(control_frame, text="Status:", font=self.f(10, "bold")).pack(side=tk.LEFT)
self.status_label = ttk.Label(control_frame, text="Connecting to Launcher AP...", foreground="red", font=self.f(10))
self.status_label.pack(side=tk.LEFT, padx=self.s(10))
scale_frame = ttk.Frame(control_frame)
scale_frame.pack(side=tk.RIGHT, padx=self.s(10))
ttk.Label(scale_frame, text="Zoom:", font=self.f(8)).pack(side=tk.LEFT)
current_idx = self.zoom_levels.index(self.ui_scale) if self.ui_scale in self.zoom_levels else 0
self.scale_slider = ttk.Scale(scale_frame, from_=0, to=len(self.zoom_levels)-1, value=current_idx, command=self.update_scale_val)
self.scale_slider.pack(side=tk.LEFT, padx=5)
self.scale_slider.bind("<ButtonRelease-1>", self.trigger_redraw)
ttk.Button(control_frame, text="Save Graph", command=self.save_graph).pack(side=tk.RIGHT, padx=self.s(10))
ttk.Button(control_frame, text="Reset Data", command=self.reset_dashboard).pack(side=tk.RIGHT, padx=self.s(10))
mission_frame = ttk.LabelFrame(self.root, text="Mission Control", padding=self.s(10))
mission_frame.pack(side=tk.TOP, fill=tk.X, padx=self.s(10), pady=self.s(5))
state_container = ttk.Frame(mission_frame)
state_container.pack(side=tk.LEFT, fill=tk.Y)
ttk.Label(state_container, text="Rocket State:", font=self.f(11)).pack(side=tk.LEFT, padx=(0, self.s(10)))
dot_size = self.s(24)
self.state_canvas = tk.Canvas(state_container, width=dot_size, height=dot_size, bg=self.root.cget("bg"), highlightthickness=0)
self.state_canvas.pack(side=tk.LEFT)
self.state_dot = self.state_canvas.create_oval(self.s(4), self.s(4), dot_size-self.s(4), dot_size-self.s(4), fill="gray", outline="gray")
self.lbl_state_text = ttk.Label(state_container, text="---", font=self.f(12, "bold"))
self.lbl_state_text.pack(side=tk.LEFT, padx=self.s(10))
btn_frame = ttk.Frame(mission_frame)
btn_frame.pack(side=tk.RIGHT)
ttk.Button(btn_frame, text="CALIBRATE GYRO", command=self.send_calibrate_command, width=int(20)).pack(side=tk.LEFT, padx=self.s(5))
ttk.Button(btn_frame, text="DIGITAL LAUNCH", command=self.send_launch_command, width=int(20)).pack(side=tk.LEFT, padx=self.s(5))
tuning_frame = ttk.LabelFrame(self.root, text="PID Controller Tuning", padding=self.s(10))
tuning_frame.pack(side=tk.TOP, fill=tk.X, padx=self.s(10), pady=self.s(5))
controls_frame = ttk.Frame(tuning_frame)
controls_frame.pack(side=tk.LEFT, fill=tk.Y)
ttk.Label(controls_frame, text="Kp (Proportional):", font=self.f(10)).grid(row=0, column=0, sticky="e", padx=(0, self.s(5)), pady=self.s(2))
ttk.Spinbox(controls_frame, textvariable=self.kp_var, from_=0.0, to=10.0, increment=0.1, width=int(6*self.ui_scale), font=self.f(10, "bold")).grid(row=0, column=1, sticky="w", padx=(0, self.s(15)), pady=self.s(2))
ttk.Label(controls_frame, text="Kd (Derivative):", font=self.f(10)).grid(row=1, column=0, sticky="e", padx=(0, self.s(5)), pady=self.s(2))
ttk.Spinbox(controls_frame, textvariable=self.kd_var, from_=0.0, to=10.0, increment=0.05, width=int(6*self.ui_scale), font=self.f(10, "bold")).grid(row=1, column=1, sticky="w", padx=(0, self.s(15)), pady=self.s(2))
ttk.Button(controls_frame, text="UPLOAD\nNEW PID", command=self.send_pid_command, width=int(12*self.ui_scale)).grid(row=0, column=2, rowspan=2, sticky="ns", padx=self.s(10), pady=self.s(2))
ttk.Separator(tuning_frame, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=self.s(20))
active_frame = ttk.Frame(tuning_frame)
active_frame.pack(side=tk.LEFT, padx=self.s(10))
ttk.Label(active_frame, text="ACTIVE HARDWARE SETTINGS:", font=self.f(8, "bold"), foreground="gray").pack(side=tk.TOP, anchor="w")
display_container = tk.Frame(active_frame, bg="white", bd=1, relief=tk.SOLID, padx=self.s(10), pady=self.s(5))
display_container.pack(side=tk.TOP, anchor="w", pady=(self.s(5), 0))
self.lbl_active_pid = tk.Label(display_container, text="Kp: --.--- | Kd: --.---", font=self.fm(13), fg="#004488", bg="white")
self.lbl_active_pid.pack(side=tk.LEFT)
# --- ENVIRONMENT & LOCATION FRAME ---
env_frame = ttk.LabelFrame(self.root, text="Environment & Location", padding=self.s(10))
env_frame.pack(side=tk.TOP, fill=tk.X, padx=self.s(10), pady=self.s(5))
# New GPS State Bubble
gps_status_frame = ttk.Frame(env_frame)
gps_status_frame.grid(row=0, column=0, padx=self.s(15), sticky="ew")
ttk.Label(gps_status_frame, text="GPS Status", font=self.f(9)).pack(side=tk.TOP)
bubble_frame = ttk.Frame(gps_status_frame)
bubble_frame.pack(side=tk.TOP, pady=self.s(5))
gps_dot_size = self.s(18)
self.gps_canvas = tk.Canvas(bubble_frame, width=gps_dot_size, height=gps_dot_size, bg=self.root.cget("bg"), highlightthickness=0)
self.gps_canvas.pack(side=tk.LEFT)
self.gps_dot = self.gps_canvas.create_oval(self.s(2), self.s(2), gps_dot_size-self.s(2), gps_dot_size-self.s(2), fill="red", outline="gray")
self.lbl_gps_text = ttk.Label(bubble_frame, text="NO NMEA", font=self.f(10, "bold"), foreground="red")
self.lbl_gps_text.pack(side=tk.LEFT, padx=self.s(5))
self.lbl_alt = self.create_stat_label(env_frame, "Altitude (m ASL)", 1)
self.lbl_lat = self.create_stat_label(env_frame, "Latitude", 2)
self.lbl_lon = self.create_stat_label(env_frame, "Longitude", 3)
# --- LIVE TELEMETRY FRAME ---
stats_frame = ttk.LabelFrame(self.root, text="Live Telemetry", padding=self.s(10))
stats_frame.pack(side=tk.TOP, fill=tk.X, padx=self.s(10), pady=self.s(5))
self.lbl_time = self.create_stat_label(stats_frame, "Time (ms)", 0)
self.lbl_roll = self.create_stat_label(stats_frame, "Roll (°)", 1)
self.lbl_rate = self.create_stat_label(stats_frame, "Rate (°/s)", 2)
self.lbl_out = self.create_stat_label(stats_frame, "Servo Output", 3)
self.lbl_skew = self.create_stat_label(stats_frame, "Skew (°)", 4)
def create_stat_label(self, parent, title, col):
frame = ttk.Frame(parent)
frame.grid(row=0, column=col, padx=self.s(15), sticky="ew")
ttk.Label(frame, text=title, font=self.f(9)).pack()
value_label = ttk.Label(frame, text="---", font=self.fm(14))
value_label.pack()
return value_label
def setup_plot(self):
plt.rcParams.update({'font.size': 10 * self.ui_scale})
self.fig, self.ax = plt.subplots(figsize=(8, 4), dpi=100)
self.fig.patch.set_facecolor('#f0f0f0')
self.line_roll, = self.ax.plot([], [], label='Roll Angle', color='tab:blue', linewidth=2)
self.line_rate, = self.ax.plot([], [], label=f'Roll Rate (x{RATE_SCALE})', color='tab:orange', linewidth=1.5)
self.ax.set_title("Rocket Stability Telemetry")
self.ax.set_xlabel("Time (ms)")
self.ax.set_ylabel("Value")
self.ax.grid(True, linestyle=':', alpha=0.6)
from matplotlib.patches import Patch
self.ax.legend(handles=[self.line_roll, self.line_rate, Patch(facecolor='green', alpha=0.3, label='Servo +'), Patch(facecolor='red', alpha=0.3, label='Servo -')], loc='upper left')
self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=self.s(10), pady=self.s(10))
self.anim = FuncAnimation(self.fig, self.update_plot, interval=100, blit=False, cache_frame_data=False)
def _send_udp_command(self, cmd):
target_ip = self.rocket_ip if self.rocket_ip else LAUNCHER_GATEWAY
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(cmd.encode('utf-8'), (target_ip, UDP_PORT))
except Exception as e:
messagebox.showerror("Error", f"Failed to send '{cmd}':\n{e}")
def send_launch_command(self):
if messagebox.askyesno("CONFIRM LAUNCH", "WARNING: This bypasses the physical launch button.\nEnsure Launcher is Armed and READY.\n\nProceed to IGNITE?"):
self._send_udp_command("launch")
def send_calibrate_command(self):
if messagebox.askyesno("Confirm", "Keep rocket STILL. Zeroing Gyro. Proceed?"):
self._send_udp_command("calibrate")
def send_pid_command(self):
try:
self._send_udp_command(f"PID,{float(self.kp_var.get())},{float(self.kd_var.get())}")
except ValueError:
messagebox.showerror("Error", "Kp and Kd must be valid numbers.")
def connection_watchdog(self):
while self.running:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(b"HELLO", (LAUNCHER_GATEWAY, UDP_PORT))
except: pass
time.sleep(2)
def udp_listener(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
while self.running:
try:
data, addr = sock.recvfrom(BUFFER_SIZE)
message = data.decode('utf-8').strip()
if self.rocket_ip != addr[0]:
self.rocket_ip = addr[0]
self.root.after(0, lambda: self.status_label.config(text=f"Connected: {self.rocket_ip}", foreground="green"))
self.parse_data(message)
except: pass
def parse_data(self, message):
try:
if message.startswith("STATUS:"):
parts = message.split(":", 1)[1].strip().split(",")
self.current_values["State"] = parts[0]
if len(parts) >= 4:
self.current_values["ActiveKp"] = float(parts[1])
self.current_values["ActiveKd"] = float(parts[2])
self.current_values["Skew"] = float(parts[3])
elif message.startswith("ENV,"):
parts = message.split(',')
if len(parts) >= 4:
self.current_values["Lat"] = float(parts[1])
self.current_values["Lon"] = float(parts[2])
self.current_values["Alt"] = float(parts[3])
if len(parts) >= 5: # Safely get the new GPS state variable
self.current_values["GPS_State"] = int(parts[4])
elif message.startswith("T,") or message.startswith("[FUSION]"):
if message.startswith("[FUSION]"): return
parts = message.split(',')
if len(parts) >= 5:
t, r, rt, o = float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])
self.time_data.append(t)
self.roll_data.append(r)
self.rate_data.append(rt)
self.output_data.append(o)
self.current_values.update({"Time": t, "Roll": r, "Rate": rt, "Output": o})
state = self.current_values.get("State", "DISCONNECTED")
if state != self.last_state:
if self.last_state != "DISCONNECTED" and t > 0:
self.mission_events.append({"time": t, "state": state})
self.last_state = state
except: pass
def gui_update_loop(self):
if self.running:
self.update_stats()
self.root.after(100, self.gui_update_loop)
def update_stats(self):
if not hasattr(self, 'lbl_time') or not self.lbl_time.winfo_exists(): return
vals = self.current_values
self.lbl_time.config(text=f"{int(vals['Time'])}")
self.lbl_roll.config(text=f"{vals['Roll']:.2f}")
self.lbl_rate.config(text=f"{vals['Rate']:.2f}")
self.lbl_out.config(text=f"{vals['Output']:.2f}")
self.lbl_skew.config(text=f"{vals['Skew']:.2f}")
# New Environment UI Updates
if hasattr(self, 'lbl_alt'):
self.lbl_alt.config(text=f"{vals['Alt']:.1f}")
self.lbl_lat.config(text=f"{vals['Lat']:.6f}")
self.lbl_lon.config(text=f"{vals['Lon']:.6f}")
# Update GPS Status Bubble
gps_state = vals.get("GPS_State", 0)
if gps_state == 0:
self.gps_canvas.itemconfig(self.gps_dot, fill="red", outline="red")
self.lbl_gps_text.config(text="NO NMEA", foreground="red")
elif gps_state == 1:
self.gps_canvas.itemconfig(self.gps_dot, fill="orange", outline="orange")
self.lbl_gps_text.config(text="SEARCHING", foreground="orange")
elif gps_state == 2:
self.gps_canvas.itemconfig(self.gps_dot, fill="green", outline="green")
self.lbl_gps_text.config(text="FIX ACQUIRED", foreground="green")
state = vals["State"]
self.lbl_state_text.config(text=state)
self.lbl_active_pid.config(text=f"Kp: {vals['ActiveKp']:.3f} | Kd: {vals['ActiveKd']:.3f}")
color_map = {"IDLE": "gray", "ARMED": "#F4D03F", "IGNITING": "#FF8C00", "FLIGHT": "green", "DISCONNECTED": "gray"}
dot_color = color_map.get(state, "gray")
self.state_canvas.itemconfig(self.state_dot, fill=dot_color, outline=dot_color)
def update_plot(self, frame):
if not self.time_data: return self.line_roll, self.line_rate
t, roll, rate = list(self.time_data)[-VIEW_POINTS:], list(self.roll_data)[-VIEW_POINTS:], list(self.rate_data)[-VIEW_POINTS:]
out = np.array(list(self.output_data)[-VIEW_POINTS:])
rate_plot = [r * RATE_SCALE for r in rate]
self.line_roll.set_data(t, roll)
self.line_rate.set_data(t, rate_plot)
for collection in self.ax.collections: collection.remove()
self.ax.fill_between(t, out, 0, where=(out >= 0), interpolate=True, color='green', alpha=0.3)
self.ax.fill_between(t, out, 0, where=(out < 0), interpolate=True, color='red', alpha=0.3)
self.ax.set_xlim(min(t), max(t) + 1)
limit = 10.0
all_y = roll + rate_plot + list(out)
if all_y and max(abs(y) for y in all_y) > limit: limit = max(abs(y) for y in all_y) * 1.1
self.ax.set_ylim(-limit, limit)
return self.line_roll, self.line_rate
def reset_dashboard(self):
self.time_data.clear(); self.roll_data.clear(); self.rate_data.clear(); self.output_data.clear()
self.mission_events.clear()
self.current_values = {"Time": 0, "Roll": 0.0, "Rate": 0.0, "Output": 0.0, "State": "DISCONNECTED", "ActiveKp": 0.0, "ActiveKd": 0.0, "Skew": 0.0, "Lat": 0.0, "Lon": 0.0, "Alt": 0.0, "GPS_State": 0}
self.last_state = "DISCONNECTED"
self.update_stats()
def save_graph(self):
if hasattr(self, 'anim') and self.anim.event_source:
self.anim.event_source.stop()
file_path = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[("PNG Image", "*.png"), ("All Files", "*.*")],
title="Save Telemetry Graph"
)
if file_path:
try:
data_len = len(self.time_data)
width = max(12, min(200, data_len / 50)) if data_len > 0 else 12
save_fig, save_ax = plt.subplots(figsize=(width, 4), dpi=100)
t = list(self.time_data)
roll = list(self.roll_data)
rate = [r * RATE_SCALE for r in self.rate_data]
out = np.array(self.output_data)
save_ax.plot(t, roll, label='Roll Angle', color='tab:blue', linewidth=2)
save_ax.plot(t, rate, label=f'Roll Rate (x{RATE_SCALE})', color='tab:orange', linewidth=1.5)
save_ax.fill_between(t, out, 0, where=(out >= 0), interpolate=True, color='green', alpha=0.3)
save_ax.fill_between(t, out, 0, where=(out < 0), interpolate=True, color='red', alpha=0.3)
y_max = max(max(roll) if roll else 10, max(rate) if rate else 10) * 0.9
for event in self.mission_events:
ev_time = event["time"]
ev_name = event["state"]
save_ax.axvline(x=ev_time, color='black', linestyle='--', alpha=0.6)
save_ax.text(ev_time, y_max, f" {ev_name}", rotation=90, verticalalignment='top', fontsize=9, fontweight='bold', color='black')
save_ax.set_title(f"Rocket Flight Data - {len(t)} points")
save_ax.legend()
if t: save_ax.set_xlim(min(t), max(t) + 1)
save_fig.savefig(file_path, dpi=100, bbox_inches='tight')
plt.close(save_fig)
messagebox.showinfo("Success", "Graph saved successfully.")
except Exception as e:
print(f"Error saving graph: {e}")
if hasattr(self, 'anim') and self.anim.event_source:
self.anim.event_source.start()
def on_close(self):
self.running = False
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = TelemetryApp(root)
root.protocol("WM_DELETE_WINDOW", app.on_close)
root.mainloop()

464
Firmware/launcher.txt Normal file
View File

@@ -0,0 +1,464 @@
/*
* LAUNCHER ESP32 CODE (WiFi AP + Comm Relay + Fusion + GPS + Barometer)
* V4 - Fully Non-Blocking & GPS Status Tracking
*/
#include <Arduino.h>
#include <SPI.h>
#include <Wire.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <ESP32Servo.h>
#include <QMC5883LCompass.h>
#include <TinyGPS++.h>
#include <Adafruit_BMP085.h>
const char* ssid = "ROCKET_LAUNCHER";
const char* password = "launch_secure";
const int udpPort = 4444;
WiFiUDP udp;
IPAddress dashboardIP;
bool dashboardConnected = false;
const int RX2_PIN = 16;
const int TX2_PIN = 17;
const int SWITCH_PIN = 5;
const int BUTTON_PIN = 18;
const int LED_PIN = 23;
const int BUZZER_PIN = 2;
const int LAUNCHER_SERVO_PIN = 19;
const int I2C_SDA = 21;
const int I2C_SCL = 22;
const int GPS_RX_PIN = 4; // Using D4 for GPS RX
const int SERVO_ON = 170;
const int SERVO_OFF = 55;
enum SystemState { SAFE, ARMING, READY, IGNITING };
SystemState currentState = SAFE;
Servo launcherServo;
QMC5883LCompass compass;
TinyGPSPlus gps;
HardwareSerial SerialGPS(1); // Use Hardware Serial 1 for GPS
Adafruit_BMP085 bmp;
// Environment Filter Variables
float filteredAlt = 0.0;
const float ALT_ALPHA = 0.15; // Filter strength for Altitude (0.0 to 1.0)
unsigned long lastHeartbeatTime = 0;
const unsigned long HEARTBEAT_TIMEOUT = 2000;
float mpu_ax = 0.0, mpu_ay = 0.0, mpu_az = 0.0;
float cal_ay = 0.0, cal_az = 1.0;
const float MAG_OFFSET_X = -211.00;
const float MAG_OFFSET_Y = 897.00;
const float MAG_OFFSET_Z = -514.50;
const float MAG_SCALE_X = 0.988;
const float MAG_SCALE_Y = 0.971;
const float MAG_SCALE_Z = 1.044;
bool udpLaunchTriggered = false;
bool rocketReady = false;
bool rocketIgnited = false;
void buzzerOn() { digitalWrite(BUZZER_PIN, LOW); }
void buzzerOff() { digitalWrite(BUZZER_PIN, HIGH); }
void beep(int ms) { buzzerOn(); delay(ms); buzzerOff(); }
void successTone() { beep(150); delay(150); beep(150); }
void errorTone() { for(int i=0; i<5; i++) { beep(100); delay(100); } }
bool isSwitchArmed() { return digitalRead(SWITCH_PIN) == LOW; }
bool isButtonPressed() { return digitalRead(BUTTON_PIN) == LOW; }
void abortSequence(String reason);
void waitForSwitchReset();
void updateAndPrintFusion();
void processGPS();
void setup() {
Serial.begin(115200);
// Configure hardware serial buffer to prevent data overflow at 115200 baud
Serial2.setRxBufferSize(2048);
Serial2.begin(115200, SERIAL_8N1, RX2_PIN, TX2_PIN);
Serial2.setTimeout(20);
// Setup GPS Serial on the RX pin (ignoring TX by passing -1)
SerialGPS.begin(9600, SERIAL_8N1, GPS_RX_PIN, -1);
Serial.println("Starting WiFi Access Point...");
WiFi.softAP(ssid, password);
IPAddress IP = WiFi.softAPIP();
Serial.print("AP IP address: ");
Serial.println(IP);
udp.begin(udpPort);
Wire.begin(I2C_SDA, I2C_SCL);
compass.init();
compass.setSmoothing(10, true);
Wire.setClock(400000);
pinMode(SWITCH_PIN, INPUT_PULLUP);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
buzzerOff();
digitalWrite(LED_PIN, LOW);
launcherServo.setPeriodHertz(50);
launcherServo.attach(LAUNCHER_SERVO_PIN);
launcherServo.write(SERVO_OFF);
if (bmp.begin()) {
filteredAlt = bmp.readAltitude(); // Seed the filter
Serial.println("BMP180 initialized.");
} else {
Serial.println("Warning: BMP180 not found.");
}
// --- GPS STARTUP CHECK ---
Serial.println("Checking GPS NMEA Data...");
unsigned long gpsStart = millis();
bool gpsWorks = false;
while (millis() - gpsStart < 5000) {
while (SerialGPS.available() > 0) {
char c = SerialGPS.read();
if (c == '$') { // Standard start of an NMEA sentence
gpsWorks = true;
}
gps.encode(c);
}
if (gpsWorks) break;
delay(10);
}
if (!gpsWorks) {
Serial.println("CRITICAL: NO GPS DATA! ABORTING LAUNCHER STARTUP.");
while (true) { // Halt and play error loop
digitalWrite(LED_PIN, HIGH);
errorTone();
digitalWrite(LED_PIN, LOW);
delay(1000);
}
}
Serial.println("GPS Test Passed.");
delay(500);
successTone();
}
void sendToDashboard(String msg) {
if (dashboardConnected) {
udp.beginPacket(dashboardIP, udpPort);
udp.println(msg);
udp.endPacket();
}
}
void processGPS() {
while (SerialGPS.available() > 0) {
gps.encode(SerialGPS.read());
}
}
void processSerial2() {
while (Serial2.available()) {
String msg = Serial2.readStringUntil('\n');
msg.trim();
if (msg.indexOf("READY") != -1) {
rocketReady = true;
}
else if (msg.indexOf("IGNITED") != -1) {
rocketIgnited = true;
}
else if (msg.startsWith("DATA,") || msg.startsWith("ALIVE")) {
lastHeartbeatTime = millis();
if (msg.startsWith("DATA,")) {
int c[10]; // 10 commas for the new skew array
c[0] = msg.indexOf(',');
for(int i=1; i<10; i++) c[i] = msg.indexOf(',', c[i-1] + 1);
if (c[9] > 0) { // Check that all fields arrived
mpu_ax = msg.substring(c[0]+1, c[1]).toFloat();
mpu_ay = msg.substring(c[1]+1, c[2]).toFloat();
mpu_az = msg.substring(c[2]+1, c[3]).toFloat();
String roll = msg.substring(c[3]+1, c[4]);
String rate = msg.substring(c[4]+1, c[5]);
String out = msg.substring(c[5]+1, c[6]);
String state = msg.substring(c[6]+1, c[7]);
String kp = msg.substring(c[7]+1, c[8]);
String kd = msg.substring(c[8]+1, c[9]);
String skew = msg.substring(c[9]+1);
sendToDashboard("T," + String(millis()) + "," + roll + "," + rate + "," + out);
sendToDashboard("STATUS:" + state + "," + kp + "," + kd + "," + skew);
}
}
}
}
}
void processUDP() {
int packetSize = udp.parsePacket();
if (packetSize) {
char buffer[packetSize + 1];
udp.read(buffer, packetSize);
buffer[packetSize] = '\0';
String msg = String(buffer);
msg.trim();
if (!dashboardConnected) {
dashboardIP = udp.remoteIP();
dashboardConnected = true;
Serial.println("Dashboard Connected via WiFi!");
}
if (msg == "HELLO") {
dashboardIP = udp.remoteIP();
} else if (msg == "launch") {
udpLaunchTriggered = true;
} else if (msg == "calibrate") {
Serial2.println("CALIBRATE");
} else if (msg.startsWith("PID,")) {
Serial2.println(msg);
}
}
}
void loop() {
processUDP();
processGPS();
if (!isSwitchArmed() && currentState != SAFE) {
abortSequence("Arming switch flipped OFF.");
return;
}
switch (currentState) {
case SAFE:
launcherServo.write(SERVO_OFF);
digitalWrite(LED_PIN, LOW);
buzzerOff();
processSerial2();
if (isSwitchArmed()) {
currentState = ARMING;
Serial.println("ARMING Rocket...");
}
break;
case ARMING: {
launcherServo.write(SERVO_ON);
unsigned long armStart = millis();
rocketReady = false;
while (millis() - armStart < 8000) {
processUDP();
processSerial2();
processGPS();
if (!isSwitchArmed()) return;
if (rocketReady) break;
delay(10);
}
if (!rocketReady) { abortSequence("Rocket Timeout."); return; }
for(int i = 0; i < 3; i++) {
digitalWrite(LED_PIN, HIGH); beep(80); digitalWrite(LED_PIN, LOW); delay(80);
}
delay(1000);
float sumAy = 0, sumAz = 0;
int samples = 0;
unsigned long startWait = millis();
while(millis() - startWait < 150) {
processSerial2();
processGPS();
sumAy += mpu_ay;
sumAz += mpu_az;
samples++;
delay(5);
}
if (samples > 0) {
float L = sqrt((sumAy/samples)*(sumAy/samples) + (sumAz/samples)*(sumAz/samples));
if (L > 0.1) { cal_ay = (sumAy/samples) / L; cal_az = (sumAz/samples) / L; }
}
currentState = READY;
lastHeartbeatTime = millis();
digitalWrite(LED_PIN, HIGH);
Serial2.println("ARM");
successTone();
break;
}
case READY: {
processSerial2();
if (millis() - lastHeartbeatTime > HEARTBEAT_TIMEOUT) {
abortSequence("Lost heartbeat from Rocket!");
return;
}
bool triggered = false;
if (udpLaunchTriggered) {
triggered = true;
udpLaunchTriggered = false;
} else if (isButtonPressed()) {
unsigned long triggerStart = millis();
buzzerOn();
while (isButtonPressed() && millis() - triggerStart < 1000) {
processUDP();
processSerial2();
processGPS();
if (!isSwitchArmed()) { buzzerOff(); return; }
delay(10);
}
buzzerOff();
if (isButtonPressed()) triggered = true;
}
if (triggered) {
currentState = IGNITING;
Serial2.println("CALIBRATE");
delay(20);
Serial2.println("IGNITE");
Serial.println("IGNITION COMMAND SENT");
}
break;
}
case IGNITING: {
unsigned long igniteStart = millis();
rocketIgnited = false;
while (millis() - igniteStart < 5000) {
processUDP();
processSerial2();
processGPS();
if (rocketIgnited) break;
delay(10);
}
if (rocketIgnited) {
beep(200);
digitalWrite(LED_PIN, LOW);
waitForSwitchReset();
} else {
abortSequence("No IGNITED ACK received.");
}
break;
}
}
updateAndPrintFusion();
// Transmit Environment and GPS Status regularly
static unsigned long lastEnvSend = 0;
static uint32_t lastCharsProcessed = 0;
if (millis() - lastEnvSend > 1000) {
lastEnvSend = millis();
// Simple EMA Filter for BMP Altitude
float newAlt = bmp.readAltitude();
filteredAlt = (ALT_ALPHA * newAlt) + ((1.0 - ALT_ALPHA) * filteredAlt);
float lat = 0.0, lon = 0.0;
int gpsState = 0; // 0=Red(No NMEA), 1=Orange(Searching), 2=Green(Fix)
uint32_t currentChars = gps.charsProcessed();
if (currentChars == lastCharsProcessed) {
gpsState = 0; // Module stopped sending or disconnected
} else if (!gps.location.isValid() || gps.location.lat() == 0.0) {
gpsState = 1; // Seeing NMEA sentences, but no satellite fix yet
} else {
gpsState = 2; // Full fix!
lat = gps.location.lat();
lon = gps.location.lng();
}
lastCharsProcessed = currentChars;
// Send: ENV,lat,lon,alt,gpsState
char envMsg[128];
snprintf(envMsg, sizeof(envMsg), "ENV,%.6f,%.6f,%.1f,%d", lat, lon, filteredAlt, gpsState);
sendToDashboard(String(envMsg));
}
}
void updateAndPrintFusion() {
static unsigned long lastFusionTime = 0;
if (millis() - lastFusionTime < 500) return;
lastFusionTime = millis();
compass.read();
int qmc_x_raw = compass.getX();
int qmc_y_raw = compass.getY();
int qmc_z_raw = compass.getZ();
if (qmc_x_raw == 0 && qmc_y_raw == 0 && qmc_z_raw == 0) {
Wire.end(); delay(50); Wire.begin(I2C_SDA, I2C_SCL);
compass.init(); compass.setSmoothing(10, true);
Wire.setClock(400000);
return;
}
float cal_x = (qmc_x_raw - MAG_OFFSET_X) * MAG_SCALE_X;
float cal_y = (qmc_y_raw - MAG_OFFSET_Y) * MAG_SCALE_Y;
float cal_z = (qmc_z_raw - MAG_OFFSET_Z) * MAG_SCALE_Z;
float g_fwd = mpu_ax;
float g_right = mpu_az * cal_ay - mpu_ay * cal_az;
float g_down = mpu_ay * cal_ay + mpu_az * cal_az;
float pitch = atan2(-g_fwd, sqrt(g_right * g_right + g_down * g_down));
float roll = atan2(g_right, g_down);
float x_m = cal_y * cos(pitch) + cal_z * sin(roll) * sin(pitch) + cal_x * cos(roll) * sin(pitch);
float y_m = cal_z * cos(roll) - cal_x * sin(roll);
float heading = atan2(y_m, x_m) * 180.0 / PI;
if (heading < 0) heading += 360;
char fusionMsg[128];
snprintf(fusionMsg, sizeof(fusionMsg), "[FUSION] Hdg: %03.1f | Pitch: %+02.1f", heading, pitch * 180.0/PI);
sendToDashboard(String(fusionMsg));
}
void abortSequence(String reason) {
launcherServo.write(SERVO_OFF);
digitalWrite(LED_PIN, LOW);
errorTone();
waitForSwitchReset();
}
void waitForSwitchReset() {
currentState = SAFE;
unsigned long lastBlink = 0;
bool ledState = false;
while (isSwitchArmed()) {
processUDP();
processSerial2();
processGPS();
if (millis() - lastBlink > 100) {
lastBlink = millis();
ledState = !ledState;
digitalWrite(LED_PIN, ledState ? HIGH : LOW);
}
}
digitalWrite(LED_PIN, LOW);
}

411
Firmware/rocket.txt Normal file
View File

@@ -0,0 +1,411 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import socket
import threading
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from collections import deque
import time
import numpy as np
# --- Configuration ---
UDP_IP = "0.0.0.0"
UDP_PORT = 4444
BUFFER_SIZE = 1024
VIEW_POINTS = 100
RATE_SCALE = 0.25
LAUNCHER_GATEWAY = "192.168.4.1"
class TelemetryApp:
def __init__(self, root):
self.root = root
self.root.title("Rocket Telemetry Dashboard - Ground Control")
self.root.geometry("1100x850")
self.zoom_levels = [1.0, 1.5, 2.0, 2.5]
self.ui_scale = 1.0
self.time_data = deque()
self.roll_data = deque()
self.rate_data = deque()
self.output_data = deque()
self.current_values = {
"Time": 0, "Roll": 0.0, "Rate": 0.0, "Output": 0.0,
"State": "DISCONNECTED", "ActiveKp": 0.0, "ActiveKd": 0.0,
"Skew": 0.0,
"Lat": 0.0, "Lon": 0.0, "Alt": 0.0, "GPS_State": 0
}
self.mission_events = []
self.last_state = "DISCONNECTED"
self.kp_var = tk.StringVar(value="0.5")
self.kd_var = tk.StringVar(value="0.2")
self.rocket_ip = None
self.running = True
self.build_gui()
self.listener_thread = threading.Thread(target=self.udp_listener, daemon=True)
self.listener_thread.start()
self.watchdog_thread = threading.Thread(target=self.connection_watchdog, daemon=True)
self.watchdog_thread.start()
self.gui_update_loop()
def s(self, val): return int(val * self.ui_scale)
def f(self, size, weight="normal"): return ("Helvetica", self.s(size), weight)
def fm(self, size): return ("Courier New", self.s(size), "bold")
def build_gui(self):
style = ttk.Style()
style.configure('TButton', font=('Helvetica', self.s(10)))
style.configure('TLabelframe.Label', font=('Helvetica', self.s(10), 'bold'))
for widget in self.root.winfo_children(): widget.destroy()
if hasattr(self, 'anim') and self.anim and self.anim.event_source:
self.anim.event_source.stop()
self.setup_ui()
self.setup_plot()
def update_scale_val(self, val): pass
def trigger_redraw(self, event):
val = self.scale_slider.get()
idx = int(round(val))
self.scale_slider.set(idx)
self.ui_scale = self.zoom_levels[idx]
self.build_gui()
def setup_ui(self):
control_frame = ttk.Frame(self.root, padding=self.s(10))
control_frame.pack(side=tk.TOP, fill=tk.X)
ttk.Label(control_frame, text="Status:", font=self.f(10, "bold")).pack(side=tk.LEFT)
self.status_label = ttk.Label(control_frame, text="Connecting to Launcher AP...", foreground="red", font=self.f(10))
self.status_label.pack(side=tk.LEFT, padx=self.s(10))
scale_frame = ttk.Frame(control_frame)
scale_frame.pack(side=tk.RIGHT, padx=self.s(10))
ttk.Label(scale_frame, text="Zoom:", font=self.f(8)).pack(side=tk.LEFT)
current_idx = self.zoom_levels.index(self.ui_scale) if self.ui_scale in self.zoom_levels else 0
self.scale_slider = ttk.Scale(scale_frame, from_=0, to=len(self.zoom_levels)-1, value=current_idx, command=self.update_scale_val)
self.scale_slider.pack(side=tk.LEFT, padx=5)
self.scale_slider.bind("<ButtonRelease-1>", self.trigger_redraw)
ttk.Button(control_frame, text="Save Graph", command=self.save_graph).pack(side=tk.RIGHT, padx=self.s(10))
ttk.Button(control_frame, text="Reset Data", command=self.reset_dashboard).pack(side=tk.RIGHT, padx=self.s(10))
mission_frame = ttk.LabelFrame(self.root, text="Mission Control", padding=self.s(10))
mission_frame.pack(side=tk.TOP, fill=tk.X, padx=self.s(10), pady=self.s(5))
state_container = ttk.Frame(mission_frame)
state_container.pack(side=tk.LEFT, fill=tk.Y)
ttk.Label(state_container, text="Rocket State:", font=self.f(11)).pack(side=tk.LEFT, padx=(0, self.s(10)))
dot_size = self.s(24)
self.state_canvas = tk.Canvas(state_container, width=dot_size, height=dot_size, bg=self.root.cget("bg"), highlightthickness=0)
self.state_canvas.pack(side=tk.LEFT)
self.state_dot = self.state_canvas.create_oval(self.s(4), self.s(4), dot_size-self.s(4), dot_size-self.s(4), fill="gray", outline="gray")
self.lbl_state_text = ttk.Label(state_container, text="---", font=self.f(12, "bold"))
self.lbl_state_text.pack(side=tk.LEFT, padx=self.s(10))
btn_frame = ttk.Frame(mission_frame)
btn_frame.pack(side=tk.RIGHT)
ttk.Button(btn_frame, text="CALIBRATE GYRO", command=self.send_calibrate_command, width=int(20)).pack(side=tk.LEFT, padx=self.s(5))
ttk.Button(btn_frame, text="DIGITAL LAUNCH", command=self.send_launch_command, width=int(20)).pack(side=tk.LEFT, padx=self.s(5))
tuning_frame = ttk.LabelFrame(self.root, text="PID Controller Tuning", padding=self.s(10))
tuning_frame.pack(side=tk.TOP, fill=tk.X, padx=self.s(10), pady=self.s(5))
controls_frame = ttk.Frame(tuning_frame)
controls_frame.pack(side=tk.LEFT, fill=tk.Y)
ttk.Label(controls_frame, text="Kp (Proportional):", font=self.f(10)).grid(row=0, column=0, sticky="e", padx=(0, self.s(5)), pady=self.s(2))
ttk.Spinbox(controls_frame, textvariable=self.kp_var, from_=0.0, to=10.0, increment=0.1, width=int(6*self.ui_scale), font=self.f(10, "bold")).grid(row=0, column=1, sticky="w", padx=(0, self.s(15)), pady=self.s(2))
ttk.Label(controls_frame, text="Kd (Derivative):", font=self.f(10)).grid(row=1, column=0, sticky="e", padx=(0, self.s(5)), pady=self.s(2))
ttk.Spinbox(controls_frame, textvariable=self.kd_var, from_=0.0, to=10.0, increment=0.05, width=int(6*self.ui_scale), font=self.f(10, "bold")).grid(row=1, column=1, sticky="w", padx=(0, self.s(15)), pady=self.s(2))
ttk.Button(controls_frame, text="UPLOAD\nNEW PID", command=self.send_pid_command, width=int(12*self.ui_scale)).grid(row=0, column=2, rowspan=2, sticky="ns", padx=self.s(10), pady=self.s(2))
ttk.Separator(tuning_frame, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=self.s(20))
active_frame = ttk.Frame(tuning_frame)
active_frame.pack(side=tk.LEFT, padx=self.s(10))
ttk.Label(active_frame, text="ACTIVE HARDWARE SETTINGS:", font=self.f(8, "bold"), foreground="gray").pack(side=tk.TOP, anchor="w")
display_container = tk.Frame(active_frame, bg="white", bd=1, relief=tk.SOLID, padx=self.s(10), pady=self.s(5))
display_container.pack(side=tk.TOP, anchor="w", pady=(self.s(5), 0))
self.lbl_active_pid = tk.Label(display_container, text="Kp: --.--- | Kd: --.---", font=self.fm(13), fg="#004488", bg="white")
self.lbl_active_pid.pack(side=tk.LEFT)
# --- ENVIRONMENT & LOCATION FRAME ---
env_frame = ttk.LabelFrame(self.root, text="Environment & Location", padding=self.s(10))
env_frame.pack(side=tk.TOP, fill=tk.X, padx=self.s(10), pady=self.s(5))
# New GPS State Bubble
gps_status_frame = ttk.Frame(env_frame)
gps_status_frame.grid(row=0, column=0, padx=self.s(15), sticky="ew")
ttk.Label(gps_status_frame, text="GPS Status", font=self.f(9)).pack(side=tk.TOP)
bubble_frame = ttk.Frame(gps_status_frame)
bubble_frame.pack(side=tk.TOP, pady=self.s(5))
gps_dot_size = self.s(18)
self.gps_canvas = tk.Canvas(bubble_frame, width=gps_dot_size, height=gps_dot_size, bg=self.root.cget("bg"), highlightthickness=0)
self.gps_canvas.pack(side=tk.LEFT)
self.gps_dot = self.gps_canvas.create_oval(self.s(2), self.s(2), gps_dot_size-self.s(2), gps_dot_size-self.s(2), fill="red", outline="gray")
self.lbl_gps_text = ttk.Label(bubble_frame, text="NO NMEA", font=self.f(10, "bold"), foreground="red")
self.lbl_gps_text.pack(side=tk.LEFT, padx=self.s(5))
self.lbl_alt = self.create_stat_label(env_frame, "Altitude (m ASL)", 1)
self.lbl_lat = self.create_stat_label(env_frame, "Latitude", 2)
self.lbl_lon = self.create_stat_label(env_frame, "Longitude", 3)
# --- LIVE TELEMETRY FRAME ---
stats_frame = ttk.LabelFrame(self.root, text="Live Telemetry", padding=self.s(10))
stats_frame.pack(side=tk.TOP, fill=tk.X, padx=self.s(10), pady=self.s(5))
self.lbl_time = self.create_stat_label(stats_frame, "Time (ms)", 0)
self.lbl_roll = self.create_stat_label(stats_frame, "Roll (°)", 1)
self.lbl_rate = self.create_stat_label(stats_frame, "Rate (°/s)", 2)
self.lbl_out = self.create_stat_label(stats_frame, "Servo Output", 3)
self.lbl_skew = self.create_stat_label(stats_frame, "Skew (°)", 4)
def create_stat_label(self, parent, title, col):
frame = ttk.Frame(parent)
frame.grid(row=0, column=col, padx=self.s(15), sticky="ew")
ttk.Label(frame, text=title, font=self.f(9)).pack()
value_label = ttk.Label(frame, text="---", font=self.fm(14))
value_label.pack()
return value_label
def setup_plot(self):
plt.rcParams.update({'font.size': 10 * self.ui_scale})
self.fig, self.ax = plt.subplots(figsize=(8, 4), dpi=100)
self.fig.patch.set_facecolor('#f0f0f0')
self.line_roll, = self.ax.plot([], [], label='Roll Angle', color='tab:blue', linewidth=2)
self.line_rate, = self.ax.plot([], [], label=f'Roll Rate (x{RATE_SCALE})', color='tab:orange', linewidth=1.5)
self.ax.set_title("Rocket Stability Telemetry")
self.ax.set_xlabel("Time (ms)")
self.ax.set_ylabel("Value")
self.ax.grid(True, linestyle=':', alpha=0.6)
from matplotlib.patches import Patch
self.ax.legend(handles=[self.line_roll, self.line_rate, Patch(facecolor='green', alpha=0.3, label='Servo +'), Patch(facecolor='red', alpha=0.3, label='Servo -')], loc='upper left')
self.canvas = FigureCanvasTkAgg(self.fig, master=self.root)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=self.s(10), pady=self.s(10))
self.anim = FuncAnimation(self.fig, self.update_plot, interval=100, blit=False, cache_frame_data=False)
def _send_udp_command(self, cmd):
target_ip = self.rocket_ip if self.rocket_ip else LAUNCHER_GATEWAY
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(cmd.encode('utf-8'), (target_ip, UDP_PORT))
except Exception as e:
messagebox.showerror("Error", f"Failed to send '{cmd}':\n{e}")
def send_launch_command(self):
if messagebox.askyesno("CONFIRM LAUNCH", "WARNING: This bypasses the physical launch button.\nEnsure Launcher is Armed and READY.\n\nProceed to IGNITE?"):
self._send_udp_command("launch")
def send_calibrate_command(self):
if messagebox.askyesno("Confirm", "Keep rocket STILL. Zeroing Gyro. Proceed?"):
self._send_udp_command("calibrate")
def send_pid_command(self):
try:
self._send_udp_command(f"PID,{float(self.kp_var.get())},{float(self.kd_var.get())}")
except ValueError:
messagebox.showerror("Error", "Kp and Kd must be valid numbers.")
def connection_watchdog(self):
while self.running:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(b"HELLO", (LAUNCHER_GATEWAY, UDP_PORT))
except: pass
time.sleep(2)
def udp_listener(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
while self.running:
try:
data, addr = sock.recvfrom(BUFFER_SIZE)
message = data.decode('utf-8').strip()
if self.rocket_ip != addr[0]:
self.rocket_ip = addr[0]
self.root.after(0, lambda: self.status_label.config(text=f"Connected: {self.rocket_ip}", foreground="green"))
self.parse_data(message)
except: pass
def parse_data(self, message):
try:
if message.startswith("STATUS:"):
parts = message.split(":", 1)[1].strip().split(",")
self.current_values["State"] = parts[0]
if len(parts) >= 4:
self.current_values["ActiveKp"] = float(parts[1])
self.current_values["ActiveKd"] = float(parts[2])
self.current_values["Skew"] = float(parts[3])
elif message.startswith("ENV,"):
parts = message.split(',')
if len(parts) >= 4:
self.current_values["Lat"] = float(parts[1])
self.current_values["Lon"] = float(parts[2])
self.current_values["Alt"] = float(parts[3])
if len(parts) >= 5: # Safely get the new GPS state variable
self.current_values["GPS_State"] = int(parts[4])
elif message.startswith("T,") or message.startswith("[FUSION]"):
if message.startswith("[FUSION]"): return
parts = message.split(',')
if len(parts) >= 5:
t, r, rt, o = float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4])
self.time_data.append(t)
self.roll_data.append(r)
self.rate_data.append(rt)
self.output_data.append(o)
self.current_values.update({"Time": t, "Roll": r, "Rate": rt, "Output": o})
state = self.current_values.get("State", "DISCONNECTED")
if state != self.last_state:
if self.last_state != "DISCONNECTED" and t > 0:
self.mission_events.append({"time": t, "state": state})
self.last_state = state
except: pass
def gui_update_loop(self):
if self.running:
self.update_stats()
self.root.after(100, self.gui_update_loop)
def update_stats(self):
if not hasattr(self, 'lbl_time') or not self.lbl_time.winfo_exists(): return
vals = self.current_values
self.lbl_time.config(text=f"{int(vals['Time'])}")
self.lbl_roll.config(text=f"{vals['Roll']:.2f}")
self.lbl_rate.config(text=f"{vals['Rate']:.2f}")
self.lbl_out.config(text=f"{vals['Output']:.2f}")
self.lbl_skew.config(text=f"{vals['Skew']:.2f}")
# New Environment UI Updates
if hasattr(self, 'lbl_alt'):
self.lbl_alt.config(text=f"{vals['Alt']:.1f}")
self.lbl_lat.config(text=f"{vals['Lat']:.6f}")
self.lbl_lon.config(text=f"{vals['Lon']:.6f}")
# Update GPS Status Bubble
gps_state = vals.get("GPS_State", 0)
if gps_state == 0:
self.gps_canvas.itemconfig(self.gps_dot, fill="red", outline="red")
self.lbl_gps_text.config(text="NO NMEA", foreground="red")
elif gps_state == 1:
self.gps_canvas.itemconfig(self.gps_dot, fill="orange", outline="orange")
self.lbl_gps_text.config(text="SEARCHING", foreground="orange")
elif gps_state == 2:
self.gps_canvas.itemconfig(self.gps_dot, fill="green", outline="green")
self.lbl_gps_text.config(text="FIX ACQUIRED", foreground="green")
state = vals["State"]
self.lbl_state_text.config(text=state)
self.lbl_active_pid.config(text=f"Kp: {vals['ActiveKp']:.3f} | Kd: {vals['ActiveKd']:.3f}")
color_map = {"IDLE": "gray", "ARMED": "#F4D03F", "IGNITING": "#FF8C00", "FLIGHT": "green", "DISCONNECTED": "gray"}
dot_color = color_map.get(state, "gray")
self.state_canvas.itemconfig(self.state_dot, fill=dot_color, outline=dot_color)
def update_plot(self, frame):
if not self.time_data: return self.line_roll, self.line_rate
t, roll, rate = list(self.time_data)[-VIEW_POINTS:], list(self.roll_data)[-VIEW_POINTS:], list(self.rate_data)[-VIEW_POINTS:]
out = np.array(list(self.output_data)[-VIEW_POINTS:])
rate_plot = [r * RATE_SCALE for r in rate]
self.line_roll.set_data(t, roll)
self.line_rate.set_data(t, rate_plot)
for collection in self.ax.collections: collection.remove()
self.ax.fill_between(t, out, 0, where=(out >= 0), interpolate=True, color='green', alpha=0.3)
self.ax.fill_between(t, out, 0, where=(out < 0), interpolate=True, color='red', alpha=0.3)
self.ax.set_xlim(min(t), max(t) + 1)
limit = 10.0
all_y = roll + rate_plot + list(out)
if all_y and max(abs(y) for y in all_y) > limit: limit = max(abs(y) for y in all_y) * 1.1
self.ax.set_ylim(-limit, limit)
return self.line_roll, self.line_rate
def reset_dashboard(self):
self.time_data.clear(); self.roll_data.clear(); self.rate_data.clear(); self.output_data.clear()
self.mission_events.clear()
self.current_values = {"Time": 0, "Roll": 0.0, "Rate": 0.0, "Output": 0.0, "State": "DISCONNECTED", "ActiveKp": 0.0, "ActiveKd": 0.0, "Skew": 0.0, "Lat": 0.0, "Lon": 0.0, "Alt": 0.0, "GPS_State": 0}
self.last_state = "DISCONNECTED"
self.update_stats()
def save_graph(self):
if hasattr(self, 'anim') and self.anim.event_source:
self.anim.event_source.stop()
file_path = filedialog.asksaveasfilename(
defaultextension=".png",
filetypes=[("PNG Image", "*.png"), ("All Files", "*.*")],
title="Save Telemetry Graph"
)
if file_path:
try:
data_len = len(self.time_data)
width = max(12, min(200, data_len / 50)) if data_len > 0 else 12
save_fig, save_ax = plt.subplots(figsize=(width, 4), dpi=100)
t = list(self.time_data)
roll = list(self.roll_data)
rate = [r * RATE_SCALE for r in self.rate_data]
out = np.array(self.output_data)
save_ax.plot(t, roll, label='Roll Angle', color='tab:blue', linewidth=2)
save_ax.plot(t, rate, label=f'Roll Rate (x{RATE_SCALE})', color='tab:orange', linewidth=1.5)
save_ax.fill_between(t, out, 0, where=(out >= 0), interpolate=True, color='green', alpha=0.3)
save_ax.fill_between(t, out, 0, where=(out < 0), interpolate=True, color='red', alpha=0.3)
y_max = max(max(roll) if roll else 10, max(rate) if rate else 10) * 0.9
for event in self.mission_events:
ev_time = event["time"]
ev_name = event["state"]
save_ax.axvline(x=ev_time, color='black', linestyle='--', alpha=0.6)
save_ax.text(ev_time, y_max, f" {ev_name}", rotation=90, verticalalignment='top', fontsize=9, fontweight='bold', color='black')
save_ax.set_title(f"Rocket Flight Data - {len(t)} points")
save_ax.legend()
if t: save_ax.set_xlim(min(t), max(t) + 1)
save_fig.savefig(file_path, dpi=100, bbox_inches='tight')
plt.close(save_fig)
messagebox.showinfo("Success", "Graph saved successfully.")
except Exception as e:
print(f"Error saving graph: {e}")
if hasattr(self, 'anim') and self.anim.event_source:
self.anim.event_source.start()
def on_close(self):
self.running = False
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = TelemetryApp(root)
root.protocol("WM_DELETE_WINDOW", app.on_close)
root.mainloop()

64
README.md Normal file
View File

@@ -0,0 +1,64 @@
\# MANPADS Rocket \& Launcher Prototype
\*\*30-Second Project Overview (Short)\*\*
https://www.youtube.com/shorts/zFn\_\_6\_LdTc
\*\*Full System Overview (5 minutes)\*\*
https://www.youtube.com/watch?v=DDO2EvXyncE\&t=54s
\*\*Full Development Media and Documentation\*\*
https://drive.google.com/drive/folders/17zpks6\_R59H0iXJaGkTrtp1SzIFFAQtY?usp=sharing
The Google Drive archive contains additional development media and documentation, including:
\- mechanical design and assembly
\- system electronics and firmware testing
\- launch tests and rocket motor development
\- system flow diagram
\- rocket specifications
\- bill of materials and cost breakdown
\## Project Overview
This project is a proof-of-concept prototype of a low-cost rocket launcher and guided rocket system built using consumer electronics and 3D-printed components. The rocket uses folding fins and canard stabilization controlled by an onboard ESP32 flight computer and MPU6050 inertial measurement unit, while the launcher integrates sensors such as GPS, compass, and barometric modules for orientation and telemetry. The system was designed in Fusion 360, simulated using OpenRocket, and developed through iterative mechanical, electronics, and launch testing. The total hardware cost of the prototype is approximately \*\*$96\*\*.
\## Repository Contents
This repository contains the core engineering components of the project:
\- mechanical CAD files for the rocket and launcher
\- firmware source code for the rocket flight controller and launcher system
\- OpenRocket simulation files used for aerodynamic stability analysis
\- supporting project documentation

Binary file not shown.

BIN
Simulation/Side View.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
Simulation/fdsdf.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB