Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#include <RadioLib.h>
#include <TinyGPS++.h> // Mikal Hart v1.0.3
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// 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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,73 +5,79 @@
#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() {
if(packetAvailable){
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();
}
Loading