diff --git a/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Heltec_V3_U-BloxGPS_SD-card.ino b/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Heltec_V3_U-BloxGPS_SD-card.ino new file mode 100644 index 0000000..02217cc --- /dev/null +++ b/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Heltec_V3_U-BloxGPS_SD-card.ino @@ -0,0 +1,187 @@ +#include +#include // Mikal Hart v1.0.3 +#include +#include +#include +#include +#include + +// Heltec V3 Internal Pins +#define VEXT_PIN 36 +#define OLED_RST 21 +#define LORA_NSS 8 +#define LORA_DIO1 14 +#define LORA_NRST 12 +#define LORA_BUSY 13 + +// SD Card Pins - Using separate HSPI bus +#define SD_CS 7 +#define SD_SCK 4 +#define SD_MISO 5 +#define SD_MOSI 6 + +SPIClass hspi(HSPI); +SX1262 lora = new Module(LORA_NSS, LORA_DIO1, LORA_NRST, LORA_BUSY); +TinyGPSPlus gps; +HardwareSerial SerialGPS(2); +Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST); + +volatile bool txBusy = false; +unsigned long lastActionTime = 0; +bool sdEnabled = false; + +// Interrupt for non-blocking LoRa transmission +void onTxDone(void) { + txBusy = false; +} + +// Function to handle SD Mounting and Recovery +void trySDInit() { + if (SD.begin(SD_CS, hspi, 4000000)) { + sdEnabled = true; + File file = SD.open("/flight.csv", FILE_APPEND); + if(file) { + file.println("--- LOG SESSION START ---"); + file.println("DATE,TIME,LAT,LON,ALT_FT,SATS"); + file.close(); + } + } else { + sdEnabled = false; + } +} + +void setup() { + Serial.begin(115200); + + // 1. Hardware Power/Reset + pinMode(VEXT_PIN, OUTPUT); + digitalWrite(VEXT_PIN, LOW); + pinMode(OLED_RST, OUTPUT); + digitalWrite(OLED_RST, LOW); delay(50); digitalWrite(OLED_RST, HIGH); + + // 2. Initialize OLED + Wire.begin(17, 18); + display.begin(SSD1306_SWITCHCAPVCC, 0x3C); + display.clearDisplay(); + display.setTextColor(WHITE); + display.setCursor(0,0); + display.println("BALLOON SYSTEM BOOT"); + display.display(); + + // 3. Initialize SD (HSPI Bus) + hspi.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS); + trySDInit(); + display.println(sdEnabled ? "SD: READY" : "SD: NOT FOUND"); + display.display(); + + // 4. Initialize GPS + SerialGPS.begin(9600, SERIAL_8N1, 45, 46); + // UBX Airborne < 1g Mode Command + byte packet[] = {0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0xFF, 0xFF, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x10, 0x27, 0x00, 0x00, 0x05, 0x00, 0xFA, 0x00, 0xFA, 0x00, 0x64, 0x00, 0x2C, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0xDC}; + SerialGPS.write(packet, sizeof(packet)); + display.println("GPS: AIRBORNE SET"); + display.display(); + + // 5. Initialize LoRa + // freq: 915.0, bw: 125.0, sf: 7, cr: 5, sync: 0x12, pwr: 10, preamble: 8, tcxo: 1.6V, LDO: false (DC-DC) + int status = lora.begin(915.0, 125.0, 7, 5, 0x12, 10, 8, 1.6, false); + if (status == RADIOLIB_ERR_NONE) { + display.print("LORA PING... "); + display.display(); + + // Blocking startup ping + int txStatus = lora.transmit("BALLOON_ONLINE_STARTUP"); + + if(txStatus == RADIOLIB_ERR_NONE) display.println("SENT"); + else display.println("FAIL"); + + lora.setPacketSentAction(onTxDone); + } else { + display.printf("LORA ERR: %d\n", status); + } + display.display(); + delay(2000); +} + +void loop() { + // Constant GPS Feeding + while (SerialGPS.available() > 0) { + gps.encode(SerialGPS.read()); + } + + // Action Cycle: 10 Seconds + if (millis() - lastActionTime > 10000) { + + // A. SD Recovery Check + if (!sdEnabled) { + trySDInit(); + } + + // B. Build Data String + char dataBuf[128]; + snprintf(dataBuf, sizeof(dataBuf), "%02d/%02d/%d,%02d:%02d:%02d,%.6f,%.6f,%.1f,%d", + gps.date.month(), gps.date.day(), gps.date.year(), + gps.time.hour(), gps.time.minute(), gps.time.second(), + gps.location.lat(), gps.location.lng(), gps.altitude.feet(), gps.satellites.value()); + + // C. Write to SD + if (sdEnabled) { + File dataFile = SD.open("/flight.csv", FILE_APPEND); + if (dataFile) { + dataFile.println(dataBuf); + dataFile.close(); + } else { + sdEnabled = false; // Trigger recovery on next loop + } + } + + // D. Transmit LoRa + if (!txBusy) { + txBusy = true; + lora.startTransmit(dataBuf); + } + + lastActionTime = millis(); + } + + updateDisplay(); +} + +void updateDisplay() { + display.clearDisplay(); + display.setCursor(0, 0); + + // Date and Time (Always attempt to show) + if (gps.date.isValid() && gps.time.isValid()) { + display.printf("%02d/%02d/%02d %02d:%02d:%02d\n", + gps.date.month(), gps.date.day(), gps.date.year() % 100, + gps.time.hour(), gps.time.minute(), gps.time.second()); + } else { + display.printf("SATS: %d Wait Time...\n", gps.satellites.value()); + } + + display.println("--------------------"); + + // GPS Position Data + if (gps.location.isValid()) { + display.printf("ALT: %.0f ft\n", gps.altitude.feet()); + display.printf("LAT: %.5f\n", gps.location.lat()); + display.printf("LON: %.5f\n", gps.location.lng()); + } else { + display.println("GPS: SEARCHING..."); + display.printf("Visible Satellites: %d\n", gps.satellites.value()); + if (gps.satellites.value() > 2) display.println("Wait for lock..."); + } + + // SD Status Bar + display.setCursor(0, 56); + if (!sdEnabled) { + display.setTextColor(BLACK, WHITE); + display.print(" SD: ERROR (RETRYING) "); + display.setTextColor(WHITE, BLACK); + } else { + display.print("SD: LOGGING OK"); + } + + display.display(); +} diff --git a/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Receiver_Code.ino b/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Receiver_Code.ino index dd5a74b..3304013 100644 --- a/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Receiver_Code.ino +++ b/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Receiver_Code.ino @@ -5,22 +5,36 @@ #define LORA_DIO1 14 #define LORA_NRST 12 #define LORA_BUSY 13 +#define VEXT_CTRL 3 HT_st7735 screen; - -// initalize lora radio and make an interrupt function for when packets arrive SX1262 lora = new Module(LORA_NSS, LORA_DIO1, LORA_NRST, LORA_BUSY); + volatile bool packetAvailable = false; -ICACHE_RAM_ATTR // puts onReceive in RAM for better memory access + void onReceive(void) { packetAvailable = true; } void setup() { - Serial.begin(115000); + Serial.begin(115200); + + pinMode(VEXT_CTRL, OUTPUT); + digitalWrite(VEXT_CTRL, LOW); + delay(100); + screen.st7735_init(); screen.st7735_fill_screen(ST7735_BLACK); - loraInit(); + screen.st7735_write_str(0, 0, "RECEIVER READY", Font_7x10, ST7735_YELLOW, ST7735_BLACK); + + // Frequency: 915.0 MHz, TCXO: 1.6V, Regulator: DC-DC + int status = lora.begin(915.0, 125.0, 7, 5, 0x12, 10, 8, 1.6, false); + + if (status == RADIOLIB_ERR_NONE) { + screen.st7735_write_str(0, 15, "LORA: OK", Font_7x10, ST7735_GREEN, ST7735_BLACK); + lora.setPacketReceivedAction(onReceive); + lora.startReceive(); + } } void loop() { @@ -28,50 +42,42 @@ void loop() { readPacket(); } } - -int loraInit(){ - screen.st7735_write_str(0, 0, "LORA init...", Font_7x10, ST7735_WHITE, ST7735_BLACK); - int status1 = lora.begin(); - lora.setPacketReceivedAction(onReceive); // tells lora to call the interrupt function when a packet arrives - int status2 = lora.startReceive(); - - if (status1 == RADIOLIB_ERR_NONE && status2 == RADIOLIB_ERR_NONE) { - screen.st7735_write_str(90, 0, "OK", Font_7x10, ST7735_GREEN, ST7735_BLACK); - } else { - screen.st7735_write_str(90, 0, "ERR", Font_7x10, ST7735_RED, ST7735_BLACK); - screen.st7735_write_str(120, 0, (String)status1, Font_7x10, ST7735_WHITE, ST7735_BLACK); - screen.st7735_write_str(140, 0, (String)status2, Font_7x10, ST7735_WHITE, ST7735_BLACK); - } - return status1 + status2; // can be used for status checking: will be 0 if no errors -} void readPacket() { String data; - int status = lora.readData(data); // data is stored into "data" and status is stored is "status" + int status = lora.readData(data); + if (status == RADIOLIB_ERR_NONE) { - screen.st7735_write_str(0, 12, "Data: ", Font_7x10, ST7735_WHITE, ST7735_BLACK); - screen.st7735_write_str(64, 12, data, Font_7x10, ST7735_WHITE, ST7735_BLACK); + screen.st7735_fill_screen(ST7735_BLACK); + + // --- ROW 1: HEADER --- + screen.st7735_write_str(0, 0, "PAYLOAD:", Font_7x10, ST7735_YELLOW, ST7735_BLACK); + + // --- ROW 2: DATA (Starts at 12, allows wrapping to ~34) --- + screen.st7735_write_str(0, 12, data, Font_7x10, ST7735_WHITE, ST7735_BLACK); - String rssi = (String)lora.getRSSI(); - screen.st7735_write_str(0, 24, "RSSI: dBm", Font_7x10, ST7735_WHITE, ST7735_BLACK); - screen.st7735_write_str(64, 24, rssi, Font_7x10, ST7735_WHITE, ST7735_BLACK); + // --- ROW 3: RADIO STATS --- + // Lowered to Y=40 to clear potential 2nd line of payload text + char radioStr[40]; + sprintf(radioStr, "RSSI:%-4.0f SNR:%-3.1f", lora.getRSSI(), lora.getSNR()); + screen.st7735_write_str(0, 40, radioStr, Font_7x10, ST7735_CYAN, ST7735_BLACK); - String snr = (String)lora.getSNR(); - screen.st7735_write_str(0, 36, "SNR: dB", Font_7x10, ST7735_WHITE, ST7735_BLACK); - screen.st7735_write_str(64, 36, snr, Font_7x10, ST7735_WHITE, ST7735_BLACK); + // --- ROW 4: FREQUENCY ERROR --- + char feStr[40]; + sprintf(feStr, "FE: %-5.0f Hz", lora.getFrequencyError()); + screen.st7735_write_str(0, 53, feStr, Font_7x10, ST7735_WHITE, ST7735_BLACK); - String freqErr = (String)lora.getFrequencyError(); - screen.st7735_write_str(0, 48, "Freq Err: Hz", Font_7x10, ST7735_WHITE, ST7735_BLACK); - screen.st7735_write_str(64, 48, freqErr, Font_7x10, ST7735_WHITE, ST7735_BLACK); - - } else if (status == RADIOLIB_ERR_CRC_MISMATCH) { - screen.st7735_write_str(0, 12, "CRC Error", Font_7x10, ST7735_RED, ST7735_BLACK); - screen.st7735_write_str(0, 24, "Malformed Packet", Font_7x10, ST7735_WHITE, ST7735_BLACK); } else { - String err = (String)status; - screen.st7735_write_str(0, 12, "Rx Error:", Font_7x10, ST7735_RED, ST7735_BLACK); - screen.st7735_write_str(80, 12, err, Font_7x10, ST7735_WHITE, ST7735_BLACK); + // ERROR HANDLING (Screen turns red text on failure) + screen.st7735_fill_screen(ST7735_BLACK); + screen.st7735_write_str(0, 0, "RX ERROR!", Font_7x10, ST7735_RED, ST7735_BLACK); + if(status == RADIOLIB_ERR_CRC_MISMATCH) { + screen.st7735_write_str(0, 15, "CRC CORRUPTION", Font_7x10, ST7735_RED, ST7735_BLACK); + } else { + screen.st7735_write_str(0, 15, "CODE: " + String(status), Font_7x10, ST7735_WHITE, ST7735_BLACK); + } } - packetAvailable = false; // reset flag -} + packetAvailable = false; + lora.startReceive(); +} diff --git a/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Transmitter_Code.ino b/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Transmitter_Code.ino index 58a051b..30e383c 100644 --- a/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Transmitter_Code.ino +++ b/telemetry/esp32-heltec-tracker/c/lesson-7/Lesson_7_Transmitter_Code.ino @@ -1,67 +1,138 @@ -#include "HT_st7735.h" +#include "HT_st7735.h" +#include "HT_TinyGPS++.h" #include -#define LORA_NSS 8 -#define LORA_DIO1 14 -#define LORA_NRST 12 -#define LORA_BUSY 13 +// --- Tracker v1.2 Internal GPS Pins --- +#define GPS_RX_PIN 33 // ESP32 RX <- GPS TX +#define GPS_TX_PIN 34 // ESP32 TX -> GPS RX +#define GPS_RESET 35 // GPS Reset Pin +#define VEXT_CTRL 3 // Power Control (LOW = ON) -HT_st7735 screen; +// --- LoRa Pins --- +#define LORA_NSS 8 +#define LORA_DIO1 14 +#define LORA_NRST 12 +#define LORA_BUSY 13 -// initalize lora radio and make an interrupt function for when packets finish sending +// Objects +HT_st7735 screen; SX1262 lora = new Module(LORA_NSS, LORA_DIO1, LORA_NRST, LORA_BUSY); +TinyGPSPlus gps; +HardwareSerial SerialGPS(1); + volatile bool txBusy = false; -ICACHE_RAM_ATTR // puts onReceive in RAM for better memory access +unsigned long lastActionTime = 0; +bool lastLockStatus = false; + void onTxDone(void) { txBusy = false; } -int transmissionStatus = RADIOLIB_ERR_NONE; // initial status is no errors -int packetCount = 0; void setup() { - Serial.begin(115000); + Serial.begin(115200); + + // 1. Power on hardware + pinMode(VEXT_CTRL, OUTPUT); + digitalWrite(VEXT_CTRL, LOW); + pinMode(GPS_RESET, OUTPUT); + digitalWrite(GPS_RESET, HIGH); + delay(500); + + // 2. Initialize Screen screen.st7735_init(); screen.st7735_fill_screen(ST7735_BLACK); - loraInit(); + screen.st7735_write_str(0, 0, "TRACKER V1.2", Font_7x10, ST7735_YELLOW, ST7735_BLACK); + + // 3. Initialize Internal GPS (RX=33, TX=34 @ 115200) + SerialGPS.begin(115200, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN); + + // 4. Initialize LoRa + int status = lora.begin(915.0, 125.0, 7, 5, 0x12, 10, 8, 1.6, false); + if (status == RADIOLIB_ERR_NONE) { + lora.setPacketSentAction(onTxDone); + lora.transmit("TRACKER_ONLINE"); + } + + delay(2000); + screen.st7735_fill_screen(ST7735_BLACK); } void loop() { - if(!txBusy){ - sendPacket(); + while (SerialGPS.available() > 0) { + gps.encode(SerialGPS.read()); + } + + // Detect transition from "No Lock" to "Lock" to wipe screen once to clear old text + if (gps.location.isValid() != lastLockStatus) { + lastLockStatus = gps.location.isValid(); + screen.st7735_fill_screen(ST7735_BLACK); + } + + if (millis() - lastActionTime > 10000) { + if (!txBusy && gps.location.isValid()) { + transmitData(); + } + lastActionTime = millis(); } - delay(1000); + + updateScreen(); } -int loraInit(){ - screen.st7735_write_str(0, 0, "LORA init...", Font_7x10, ST7735_WHITE, ST7735_BLACK); +void updateScreen() { + // --- ROW 1: DATE --- + if (gps.date.isValid()) { + char dStr[24]; + sprintf(dStr, "DATE: %02d/%02d/%04d", gps.date.month(), gps.date.day(), gps.date.year()); + screen.st7735_write_str(0, 0, dStr, Font_7x10, ST7735_YELLOW, ST7735_BLACK); + } else { + screen.st7735_write_str(0, 0, "DATE: SYNCING... ", Font_7x10, ST7735_WHITE, ST7735_BLACK); + } - int status = lora.begin(); - if (status == RADIOLIB_ERR_NONE) { - screen.st7735_write_str(90, 0, "OK", Font_7x10, ST7735_GREEN, ST7735_BLACK); + // --- ROW 2: TIME --- + if (gps.time.isValid()) { + char tStr[24]; + sprintf(tStr, "TIME: %02d:%02d:%02d ", gps.time.hour(), gps.time.minute(), gps.time.second()); + screen.st7735_write_str(0, 12, tStr, Font_7x10, ST7735_YELLOW, ST7735_BLACK); } else { - screen.st7735_write_str(90, 0, "ERR", Font_7x10, ST7735_RED, ST7735_BLACK); - screen.st7735_write_str(100, 0, (String)status, Font_7x10, ST7735_RED, ST7735_BLACK); + screen.st7735_write_str(0, 12, "TIME: SYNCING... ", Font_7x10, ST7735_WHITE, ST7735_BLACK); + } + + // --- GPS DATA BLOCK (Shifted Up) --- + if (gps.location.isValid()) { + char satStr[24], latStr[24], lonStr[24], altStr[24]; + + // Using %- pads the string with spaces to overwrite old characters + sprintf(satStr, "SATS: %-2d ", gps.satellites.value()); + sprintf(latStr, "LAT: %-10.5f ", gps.location.lat()); + sprintf(lonStr, "LON: %-10.5f ", gps.location.lng()); + sprintf(altStr, "ALT: %-7.0fft", gps.altitude.feet()); // Removed space before 'ft' to save width + + screen.st7735_write_str(0, 28, satStr, Font_7x10, ST7735_GREEN, ST7735_BLACK); + screen.st7735_write_str(0, 40, latStr, Font_7x10, ST7735_WHITE, ST7735_BLACK); + screen.st7735_write_str(0, 52, lonStr, Font_7x10, ST7735_WHITE, ST7735_BLACK); + screen.st7735_write_str(0, 64, altStr, Font_7x10, ST7735_CYAN, ST7735_BLACK); + } + else { + screen.st7735_write_str(0, 28, "STATUS: NO LOCK ", Font_7x10, ST7735_RED, ST7735_BLACK); + + char sCount[24]; + sprintf(sCount, "SATS VISIBLE: %-2d", gps.satellites.value()); + screen.st7735_write_str(0, 40, sCount, Font_7x10, ST7735_WHITE, ST7735_BLACK); + + // Clear the unused data lines below searching status + screen.st7735_write_str(0, 52, " ", Font_7x10, ST7735_BLACK, ST7735_BLACK); + screen.st7735_write_str(0, 64, " ", Font_7x10, ST7735_BLACK, ST7735_BLACK); } - lora.setPacketSentAction(onTxDone); - delay(2000); - return status; } -void sendPacket(){ +void transmitData() { txBusy = true; - String str = "Packet# " + String(packetCount); - packetCount++; - screen.st7735_write_str(0, 10, "Sending: ", Font_7x10, ST7735_WHITE, ST7735_BLACK); - screen.st7735_write_str(56, 10, str, Font_7x10, ST7735_WHITE, ST7735_BLACK); - transmissionStatus = lora.startTransmit(str); - delay(1000); - lora.finishTransmit(); - - if (transmissionStatus == RADIOLIB_ERR_NONE) { - String msg = "OK"; - screen.st7735_write_str(140, 10, msg, Font_7x10, ST7735_GREEN, ST7735_BLACK); - } else { - String msg = "ERR "+transmissionStatus; - screen.st7735_write_str(140, 10, msg, Font_7x10, ST7735_RED, ST7735_BLACK); - } + char payload[120]; + sprintf(payload, "%02d/%02d %02d:%02d,%.5f,%.5f,%.0f,%d", + gps.date.month(), gps.date.day(), + gps.time.hour(), gps.time.minute(), + gps.location.lat(), gps.location.lng(), + gps.altitude.feet(), gps.satellites.value()); + + lora.startTransmit(payload); }