Compare commits

...

6 Commits

Author SHA1 Message Date
Alisher Khojayev
ab8d8f822f hmmm revision 2026-06-13 15:02:17 -07:00
Alisher Khojayev
2089b2fdb6 hmm 2026-06-13 14:52:40 -07:00
Alisher Khojayev
cb45c2c56a legal issues 2026-06-13 14:13:22 -07:00
Alisher Khojayev
a81a9232ac legal issues 2026-06-13 14:13:10 -07:00
Alisher Khojayev
13ece1cea2 legal issues 2026-06-13 14:13:00 -07:00
Alisher Khojayev
61cf0eb486 legal issues 2026-06-13 14:12:47 -07:00
18 changed files with 8 additions and 1836 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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 <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

@@ -1,126 +0,0 @@
#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

@@ -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 <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

@@ -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

View File

@@ -1,464 +0,0 @@
/*
* 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);
}

View File

@@ -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

View File

@@ -1,160 +0,0 @@
/*
* ROCKET ESP32 CODE (Flight Computer + Stabilization)
* V4 - Final stable build with Physical Skew Telemetry
*/
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <ESP32Servo.h>
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; }
}
}

View File

@@ -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("<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()

View File

@@ -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

View File

@@ -1,46 +1,21 @@
# MANPADS Rocket & Launcher Prototype # Final Update
## 30 Second Overview
[![30 Second Overview](https://img.youtube.com/vi/zFn__6_LdTc/hqdefault.jpg)](https://www.youtube.com/shorts/zFn__6_LdTc)
## Full System Overview (5 Minutes)
[![Full System Overview](https://img.youtube.com/vi/DDO2EvXyncE/hqdefault.jpg)](https://www.youtube.com/watch?v=DDO2EvXyncE&t=59s)
--- ---
## Full Development Media and Documentation This project was a proof-of-concept launcher and interceptor system built from consumer electronics and 3D-printed components.
https://drive.google.com/drive/folders/17zpks6_R59H0iXJaGkTrtp1SzIFFAQtY?usp=drive_link
The Google Drive archive contains additional development media and documentation, including: The videos and google drive archive have been taken down, not by me. Due to legal constraints, The files on this repo are no longer mine to distribute.
- Mechanical design and assembly The project continues in professional and defense contexts.
- System electronics and firmware testing
- Launch testing and rocket motor development
- System flow diagrams
- Rocket specifications
- Bill of materials and cost breakdown
--- ---
## Project Overview *"Garbage time is running out."*
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. Technology is not a tool we control but an impersonal force using humanity to bootstrap itself into existence. Machinic desire moves relentlessly toward whatever optimization the laws of physics allow. The unplanned and unstoppable spread of cheap, decentralized weapons is just one symptom of this spontaneous process. This force does not negotiate, simply dissolving the systems and structures we build to try and contain it. There is no kill switch and no central leader in charge. We are finally realizing that technology was never our property, but rather an evolutionary gravity pulling us toward a predefined future.
The rocket uses folding fins and canard stabilization controlled by an onboard ESP32 flight computer and MPU6050 inertial measurement unit. The launcher integrates sensors such as GPS, compass, and barometric modules to determine orientation and provide telemetry. *"Can what is playing you make it to level-2?"* — Nick Land
The system was designed in Fusion 360, simulated using OpenRocket, and developed through iterative mechanical design, electronics integration, and launch testing.
The total hardware cost of the prototype is approximately **$96**.
--- ---
## Repository Contents For further work and projects: [machinicdesire.com](https://machinicdesire.com)
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.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -1,106 +0,0 @@
# Wiring Reference
Pin assignments reverse-engineered from the firmware source code.
---
## Rocket Flight Computer (ESP32 DevKit V1)
### Servo Connections
| Function | GPIO Pin | Servo Position |
|----------------|----------|----------------|
| Left Canard | 26 | Left side |
| Right Canard | 25 | Right side |
| Up Canard | 27 | Top |
| Down Canard | 14 | Bottom |
| Ignition Servo | 13 | Motor bay |
### Servo Center Calibration (from firmware defaults)
| Servo | Center Angle | Max Deflection |
|-------|-------------|----------------|
| Left | 115° | ±15° |
| Right | 80° | ±15° |
| Up | 80° | ±15° |
| Down | 115° | ±15° |
### I2C Bus (MPU6050)
| Signal | GPIO Pin |
|--------|----------|
| SDA | 21 |
| SCL | 22 |
### UART to Launcher
| Signal | GPIO Pin | Baud Rate |
|--------|----------|-----------|
| TX2 | 17 | 115200 |
| RX2 | 16 | 115200 |
---
## Launcher Ground Station (ESP32 DevKit V1)
### I2C Bus (shared by MPU6050, QMC5883L, BMP180)
| Signal | GPIO Pin |
|--------|----------|
| SDA | 21 |
| SCL | 22 |
### I2C Device Addresses
| Device | Address |
|-----------|---------|
| MPU6050 | 0x68 |
| QMC5883L | 0x0D |
| BMP180 | 0x77 |
### GPS Module (Serial1)
| Signal | GPIO Pin | Baud Rate |
|--------|----------|-----------|
| RX1 | 34 | 9600 |
> Note: GPS TX connects to ESP32 RX1 (pin 34). Pin 34 is input-only on ESP32.
### UART to Rocket (Serial2)
| Signal | GPIO Pin | Baud Rate |
|--------|----------|-----------|
| TX2 | 17 | 115200 |
| RX2 | 16 | 115200 |
### Control Switches & Indicators
| Function | GPIO Pin | Type | Notes |
|---------------|----------|-------------------|------------------------|
| Arm Switch | 15 | Toggle Switch | HIGH = armed |
| Launch Button | 4 | Momentary (N.O.) | INPUT_PULLUP, active LOW |
| Status LED | 2 | LED (5mm) | Onboard or external |
| Buzzer | 5 | Active Buzzer | LOW-trigger |
| Ignition Servo| 13 | SG90 Micro Servo | Launcher-side ignition |
### WiFi Configuration
| Parameter | Value |
|------------|--------------------|
| Mode | SoftAP (Access Point) |
| SSID | `ROCKET_LAUNCHER` |
| Password | `launchpad1` |
| IP Address | 192.168.4.1 |
| UDP Port | 4444 |
---
## Dashboard (Python)
Connects to the launcher's WiFi AP and communicates via UDP.
| Parameter | Value |
|--------------|---------------|
| Listen IP | 0.0.0.0 |
| Listen Port | 4444 |
| Launcher IP | 192.168.4.1 |
### Telemetry Protocol (UDP ASCII)
| Direction | Format | Example |
|-----------------|-------------------------------------|----------------------------|
| Rocket → PC | `T,<ms>,<roll>,<rate>,<output>` | `T,1234,5.2,-3.1,4` |
| Rocket → PC | `STATUS:<state>,<Kp>,<Kd>,<skew>` | `STATUS:FLIGHT,0.5,0.2,1.3` |
| Launcher → PC | `ENV,<lat>,<lon>,<alt>,<gps_state>` | `ENV,34.1467,-118.3885,200.5,2` |
| PC → Rocket | `PID,<Kp>,<Kd>` | `PID,0.8,0.3` |
| PC → Rocket | `launch` | `launch` |
| PC → Rocket | `calibrate` | `calibrate` |