diff --git a/Firmware/Calibration & Test Code/fin_calibration.ino b/Firmware/Calibration & Test Code/fin_calibration.ino deleted file mode 100644 index d7a5c7a..0000000 --- a/Firmware/Calibration & Test Code/fin_calibration.ino +++ /dev/null @@ -1,150 +0,0 @@ -/** - * ESP32 Rocket Fin Calibrator (FIXED) - * * MAPPING: - * A -> Servo 27 (UP) - * B -> Servo 14 (DOWN) - * C -> Servo 26 (RIGHT) - * D -> Servo 25 (LEFT) - */ - -#include -#include - -// ----------------------------------------------------------- -// 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(); -} \ No newline at end of file diff --git a/Firmware/Calibration & Test Code/i2c_scanner.ino b/Firmware/Calibration & Test Code/i2c_scanner.ino deleted file mode 100644 index ee67114..0000000 --- a/Firmware/Calibration & Test Code/i2c_scanner.ino +++ /dev/null @@ -1,126 +0,0 @@ -#include -#include -#include -#include - -// 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); -} \ No newline at end of file diff --git a/Firmware/Calibration & Test Code/roll_stabilization.ino b/Firmware/Calibration & Test Code/roll_stabilization.ino deleted file mode 100644 index a5b80dc..0000000 --- a/Firmware/Calibration & Test Code/roll_stabilization.ino +++ /dev/null @@ -1,339 +0,0 @@ -/* ========================================================================= - * 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 -#include -#include -#include -#include -#include -#include -#include - -// --- 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(); -} \ No newline at end of file diff --git a/Firmware/Launcher/platformio.ini b/Firmware/Launcher/platformio.ini deleted file mode 100644 index 92fed47..0000000 --- a/Firmware/Launcher/platformio.ini +++ /dev/null @@ -1,20 +0,0 @@ -; PlatformIO Project Configuration - Launcher Ground Station -; -; Build & upload: -; pio run -e launcher -t upload -; -; Monitor serial: -; pio device monitor -b 115200 - -[env:launcher] -platform = espressif32 -board = esp32dev -framework = arduino -monitor_speed = 115200 -lib_deps = - adafruit/Adafruit BMP085 Library@^1.2.4 - mprograms/QMC5883LCompass@^1.2.3 - mikalhart/TinyGPSPlus@^1.1.0 - madhephaestus/ESP32Servo@^3.0.5 - Wire -upload_speed = 921600 diff --git a/Firmware/Launcher/src/main.cpp b/Firmware/Launcher/src/main.cpp deleted file mode 100644 index 1bb36e1..0000000 --- a/Firmware/Launcher/src/main.cpp +++ /dev/null @@ -1,464 +0,0 @@ -/* - * LAUNCHER ESP32 CODE (WiFi AP + Comm Relay + Fusion + GPS + Barometer) - * V4 - Fully Non-Blocking & GPS Status Tracking - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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); -} \ No newline at end of file diff --git a/Firmware/Rocket/platformio.ini b/Firmware/Rocket/platformio.ini deleted file mode 100644 index 7e12027..0000000 --- a/Firmware/Rocket/platformio.ini +++ /dev/null @@ -1,19 +0,0 @@ -; PlatformIO Project Configuration - Rocket Flight Computer -; -; Build & upload: -; pio run -e rocket -t upload -; -; Monitor serial: -; pio device monitor -b 115200 - -[env:rocket] -platform = espressif32 -board = esp32dev -framework = arduino -monitor_speed = 115200 -lib_deps = - adafruit/Adafruit MPU6050@^2.2.6 - adafruit/Adafruit Unified Sensor@^1.1.14 - madhephaestus/ESP32Servo@^3.0.5 - Wire -upload_speed = 921600 diff --git a/Firmware/Rocket/src/main.cpp b/Firmware/Rocket/src/main.cpp deleted file mode 100644 index 646864b..0000000 --- a/Firmware/Rocket/src/main.cpp +++ /dev/null @@ -1,160 +0,0 @@ -/* - * ROCKET ESP32 CODE (Flight Computer + Stabilization) - * V4 - Final stable build with Physical Skew Telemetry - */ - -#include -#include -#include -#include -#include - -const int RX2_PIN = 16; -const int TX2_PIN = 17; -const int IGNITE_SERVO_PIN = 5; - -const int LEFT_SERVO_PIN = 26; -const int RIGHT_SERVO_PIN = 25; -const int UP_SERVO_PIN = 27; -const int DOWN_SERVO_PIN = 14; - -const int IGNITE_SERVO_ON = 150; -const int IGNITE_SERVO_OFF = 35; - -const int LEFT_CENTER = 115; -const int RIGHT_CENTER = 80; -const int UP_CENTER = 80; -const int DOWN_CENTER = 115; -const int MAX_DEFLECTION = 12; - -Servo igniteServo; -Servo leftServo, rightServo, upServo, downServo; -Adafruit_MPU6050 mpu; - -String sysState = "IDLE"; -float Kp = 0.5; -float Kd = 0.2; -String cmdBuffer = ""; - -float roll = 0; -float gyroX_offset = 0; -float physical_skew_angle = 0.0; - -unsigned long last_time; -unsigned long lastTelemetrySent = 0; -unsigned long lastReadySent = 0; -unsigned long igniteStartTime = 0; - -void calibrateGyro() { - float sumGyroX = 0, sumAccY = 0, sumAccZ = 0; - int samples = 200; - for (int i = 0; i < samples; i++) { - sensors_event_t a, g, temp; - mpu.getEvent(&a, &g, &temp); - sumGyroX += g.gyro.x; - sumAccY += a.acceleration.y; - sumAccZ += a.acceleration.z; - delay(5); - } - gyroX_offset = sumGyroX / samples; - float avgY = sumAccY / samples; - float avgZ = sumAccZ / samples; - physical_skew_angle = atan2(avgY, avgZ) * 180.0 / PI; - roll = 0.0; - last_time = millis(); -} - -void setup() { - Serial.begin(115200); - Serial2.begin(115200, SERIAL_8N1, RX2_PIN, TX2_PIN); - Serial2.setTimeout(20); - delay(1500); - Wire.begin(21, 22); - if (mpu.begin()) { - mpu.setAccelerometerRange(MPU6050_RANGE_8_G); - mpu.setGyroRange(MPU6050_RANGE_500_DEG); - mpu.setFilterBandwidth(MPU6050_BAND_10_HZ); - } - ESP32PWM::allocateTimer(0); - ESP32PWM::allocateTimer(1); - igniteServo.setPeriodHertz(50); - igniteServo.attach(IGNITE_SERVO_PIN); - igniteServo.write(IGNITE_SERVO_OFF); - leftServo.setPeriodHertz(50); leftServo.attach(LEFT_SERVO_PIN, 500, 2400); - rightServo.setPeriodHertz(50); rightServo.attach(RIGHT_SERVO_PIN, 500, 2400); - upServo.setPeriodHertz(50); upServo.attach(UP_SERVO_PIN, 500, 2400); - downServo.setPeriodHertz(50); downServo.attach(DOWN_SERVO_PIN, 500, 2400); - leftServo.write(LEFT_CENTER); - rightServo.write(RIGHT_CENTER); - upServo.write(UP_CENTER); - downServo.write(DOWN_CENTER); - calibrateGyro(); -} - -void loop() { - unsigned long current_time = millis(); - float dt = (current_time - last_time) / 1000.0; - if (dt <= 0) dt = 0.001; - last_time = current_time; - - sensors_event_t a, g, temp; - mpu.getEvent(&a, &g, &temp); - - float raw_rate_rad = g.gyro.x - gyroX_offset; - float rate_deg_s = raw_rate_rad * 180.0 / PI; - roll += rate_deg_s * dt; - - float output = (Kp * roll) + (Kd * rate_deg_s); - int servo_offset = constrain((int)output, -MAX_DEFLECTION, MAX_DEFLECTION); - - if (sysState == "FLIGHT") { - leftServo.write(LEFT_CENTER + servo_offset); - rightServo.write(RIGHT_CENTER + servo_offset); - upServo.write(UP_CENTER + servo_offset); - downServo.write(DOWN_CENTER + servo_offset); - } else { - leftServo.write(LEFT_CENTER); - rightServo.write(RIGHT_CENTER); - upServo.write(UP_CENTER); - downServo.write(DOWN_CENTER); - servo_offset = 0; - } - - if (sysState == "IGNITING" && (current_time - igniteStartTime > 2500)) { - igniteServo.write(IGNITE_SERVO_OFF); - sysState = "FLIGHT"; - Serial2.println("IGNITED"); - } - - if (current_time - lastTelemetrySent >= 50) { - // Telemetry payload including Skew Angle - String payload = "DATA," + String(a.acceleration.x, 2) + "," + - String(a.acceleration.y, 2) + "," + String(a.acceleration.z, 2) + "," + - String(roll, 2) + "," + String(rate_deg_s, 2) + "," + - String(servo_offset) + "," + sysState + "," + - String(Kp, 2) + "," + String(Kd, 2) + "," + - String(physical_skew_angle, 2); - Serial2.println(payload); - lastTelemetrySent = current_time; - } - - if (sysState == "IDLE" && (current_time - lastReadySent >= 1000)) { - Serial2.println("READY"); - lastReadySent = current_time; - } - - while (Serial2.available()) { - char c = Serial2.read(); - if (c == '\n') { - cmdBuffer.trim(); - if (cmdBuffer == "ARM" && sysState == "IDLE") { sysState = "ARMED"; calibrateGyro(); } - else if (cmdBuffer == "IGNITE") { sysState = "IGNITING"; igniteStartTime = millis(); igniteServo.write(IGNITE_SERVO_ON); } - else if (cmdBuffer == "CALIBRATE") { calibrateGyro(); } - else if (cmdBuffer.startsWith("PID,")) { - int c1 = cmdBuffer.indexOf(','), c2 = cmdBuffer.indexOf(',', c1 + 1); - if (c1 > 0 && c2 > 0) { Kp = cmdBuffer.substring(c1 + 1, c2).toFloat(); Kd = cmdBuffer.substring(c2 + 1).toFloat(); } - } - cmdBuffer = ""; - } else if (c != '\r') { cmdBuffer += c; } - } -} \ No newline at end of file diff --git a/Firmware/dashboard.py b/Firmware/dashboard.py deleted file mode 100644 index 68fb4ab..0000000 --- a/Firmware/dashboard.py +++ /dev/null @@ -1,411 +0,0 @@ -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("", 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() \ No newline at end of file diff --git a/Firmware/requirements.txt b/Firmware/requirements.txt deleted file mode 100644 index b8f615c..0000000 --- a/Firmware/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -# Rocket Telemetry Dashboard - Python Dependencies -# Install with: pip install -r requirements.txt -# -# Note: tkinter is included with most Python installations. -# On Debian/Ubuntu, install it via: sudo apt install python3-tk - -matplotlib>=3.5.0 -numpy>=1.21.0