Week 15
System Integration
Introduction
System integration is a critical step in the development of EduCanSat. At this stage, all the electronic, mechanical, and software components come together to function as a single, cohesive system. The goal is to ensure that every part—hardware, sensors, and the application—work seamlessly to deliver the intended educational experience.

Bill of Materials
Component | Model/Type | Description | Quantity | Notes |
---|---|---|---|---|
Microcontroller | XIAO ESP32-C3 | Main control unit | 1 | For all logic, processing, and comms |
GPS Module | NEO-M8N | Receives geographic position data | 1 | Connected via UART |
Temperature Sensor | BMP280 | Used for temperature only | 1 | I2C connection, pressure not used |
LoRa Module | RA-02 | Long-range wireless communication | 1 | SPI interface |
Voltage Regulator | LM2596 | Buck converter for power supply | 1 | Steps down voltage for safe operation |
Battery | BRC18650 (3.7V) | Lithium-ion battery cell | 2 | Connected in series for 7.4V |
Custom PCB | - | Custom-made circuit board | 1 | Designed and milled for this project |
3D Printed Case | PLA | Custom enclosure for components | 1 | Provides mechanical protection |
Foam Padding | - | Impact absorption | as needed | Bottom and sides of enclosure |
Male & Female Headers | - | For modular stacking/connection | as needed | Used between CanSat stages |
Fixed Battery Connector | - | Secure connector for battery | 1 | Prevents accidental disconnection |
Wiring & Assembly | - | Internal wiring and fasteners | as needed | For secure assembly |
Packaging methods
To protect and organize the internal electronics, I designed a custom 3D-printed case for EduCanSat. Foam padding was used to cushion the CanSat’s fall, and I also built a version with a reinforced bottom to further absorb impact energy during landings. The PCB is fixed securely within the enclosure using support posts, ensuring it stays in place even during drops or launches.

For communication between each layer or stage of the CanSat, I aligned male and female header connectors, eliminating the need for loose cables and preventing unwanted disconnections. Additionally, the battery system uses a fixed connector that is very hard to disconnect, even during an impact.

A sliding compartment allows easy access for maintenance or upgrades. Cable management channels prevent wires from tangling or being damaged.
Final product design
The final assembly of EduCanSat was carefully planned to ensure it looks and functions like a finished product. All modules are neatly housed within the custom 3D-printed case, with connectors and buttons accessible from the outside. The bottom part of the case was reinforced to improve impact absorption during landings. The overall enclosure is compact, robust, and designed for repeated launches and educational activities.

The modular design allows for easy assembly and disassembly, minimizing the use of external wiring. The use of aligned male and female headers between the layers ensures secure connections and facilitates maintenance or upgrades. The battery is fixed with a secure connector, reducing the risk of accidental disconnection even during hard impacts.

System integration
System integration involved connecting all the electronic modules of EduCanSat: the microcontroller, sensors (such as temperature, pressure, and GPS), LoRa communication, and the power supply. Each component was tested individually, and then as part of the complete system to ensure seamless operation.

I developed a Python application to receive, process, and visualize real-time data, including geolocation on a map. The final wiring diagram shows how each component communicates and is powered within the CanSat.

Thorough testing was performed to confirm stable data transmission and correct georeferencing during flight and landings.

Documentation link
You can find the complete system integration documentation linked from my final project page.
Source Code
This section contains the main code used in the EduCanSat project, including the firmware for both the satellite (CanSat) and the ground station, as well as the Python application for real-time data visualization.
1. EduCanSat (ESP32-C3) Firmware
#include <Wire.h>
#include <TinyGPSPlus.h>
#include <HardwareSerial.h>
#include <SPI.h>
#include <LoRa.h>
#include <Adafruit_BMP280.h>
#define I2C_SDA D2
#define I2C_SCL D3
Adafruit_BMP280 bmp;
bool bmp_ready = false;
#define GPS_RX D7
#define GPS_TX D6
HardwareSerial GPSserial(1);
TinyGPSPlus gps;
#define LORA_SCK D8
#define LORA_MISO D9
#define LORA_MOSI D10
#define LORA_CS D1
#define LORA_RST D5
#define LORA_IRQ -1
SPIClass SPI_LORA(FSPI);
unsigned long lastSendTime = 0;
const unsigned long interval = 2000;
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("🔧 Initializing LoRa + BMP280 + GPS...");
GPSserial.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);
Wire.begin(I2C_SDA, I2C_SCL);
if (bmp.begin(0x76)) {
bmp_ready = true;
Serial.println("✅ BMP280 initialized (0x76).");
} else if (bmp.begin(0x77)) {
bmp_ready = true;
Serial.println("✅ BMP280 initialized (0x77).");
} else {
Serial.println("⚠️ BMP280 not found. Skipping temperature.");
}
SPI_LORA.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
LoRa.setSPI(SPI_LORA);
LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ);
if (!LoRa.begin(433E6)) {
Serial.println("❌ LoRa init failed.");
while (true);
}
LoRa.setTxPower(20);
LoRa.setSpreadingFactor(12);
LoRa.setSignalBandwidth(125E3);
LoRa.enableCrc();
Serial.println("✅ LoRa initialized.");
}
void loop() {
while (GPSserial.available()) {
gps.encode(GPSserial.read());
}
unsigned long now = millis();
if (now - lastSendTime >= interval) {
lastSendTime = now;
if (gps.location.isValid()) {
String msg = "Lat=" + String(gps.location.lat(), 6);
msg += ",Lng=" + String(gps.location.lng(), 6);
msg += ",Sat=" + String(gps.satellites.value());
msg += ",Alt=" + String(gps.altitude.meters(), 1);
if (bmp_ready) {
float temp = bmp.readTemperature();
msg += ",T=" + String(temp, 2) + "C";
} else {
msg += ",T=N/A";
}
Serial.println("📤 Sending: " + msg);
LoRa.beginPacket();
LoRa.print(msg);
LoRa.endPacket();
} else {
Serial.println("⚠️ GPS not fixed yet...");
}
}
}
2. Ground Station (ESP32-C3) Firmware
#include <SPI.h>
#include <LoRa.h>
#define LORA_SCK D8
#define LORA_MISO D9
#define LORA_MOSI D10
#define LORA_CS D1
#define LORA_RST D5
#define LORA_IRQ -1
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("🔧 LoRa Receiver (XIAO ESP32-C3)");
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
LoRa.setSPI(SPI);
LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ);
if (!LoRa.begin(433E6)) {
Serial.println("❌ LoRa initialization failed. Check wiring.");
while (1);
}
LoRa.setSpreadingFactor(12);
LoRa.setSignalBandwidth(125E3);
LoRa.setCodingRate4(5);
LoRa.enableCrc();
Serial.println("✅ LoRa receiver ready.");
}
void loop() {
int packetSize = LoRa.parsePacket();
if (packetSize) {
if (LoRa.packetRssi() < -120) return;
if (LoRa.packetSnr() < 0) return;
Serial.print("📥 Received packet: ");
while (LoRa.available()) {
Serial.print((char)LoRa.read());
}
Serial.print(" [RSSI: ");
Serial.print(LoRa.packetRssi());
Serial.print(" dBm] [SNR: ");
Serial.print(LoRa.packetSnr());
Serial.println("]");
}
}
3. Python Visualization App (Qt Designer)
import warnings
warnings.filterwarnings("ignore")
from PyQt5 import QtWidgets, uic, QtGui, QtCore
import rasterio
from rasterio.warp import transform
import sys
import serial
import re
import time
import traceback
class SerialReader(QtCore.QThread):
data_received = QtCore.pyqtSignal(float, float, float, float, float, float, str)
def __init__(self, port="COM7", baudrate=9600):
super().__init__()
self.port = port
self.baudrate = baudrate
self.running = True
self.ser = None
def run(self):
try:
self.ser = serial.Serial(self.port, self.baudrate, timeout=1)
print(f"Serial port {self.port} opened successfully.")
except Exception as e:
print(f"Error opening serial port: {e}")
return
pattern = re.compile(
r"Lat=([-+]?\d*\.\d+|\d+),Lng=([-+]?\d*\.\d+|\d+),Sat=\d+,Alt=([-+]?\d*\.\d+|\d+),T=([-+]?\d*\.\d+|\d+)C.*?RSSI:\s*(-?\d+)\s*dBm\s*\|\s*SNR:\s*([-+]?\d*\.\d+|\d+)"
)
while self.running:
try:
if self.ser.in_waiting:
line = self.ser.readline().decode(errors='ignore').strip()
if line:
print(f"Received line: {line}")
match = pattern.search(line)
if match:
lat = float(match.group(1))
lon = float(match.group(2))
alt = float(match.group(3))
temp = float(match.group(4))
rssi = float(match.group(5))
snr = float(match.group(6))
self.data_received.emit(lat, lon, alt, temp, rssi, snr, line)
else:
time.sleep(0.1)
except Exception as e:
print(f"Error reading serial data: {e}")
traceback.print_exc()
def stop(self):
self.running = False
if self.ser and self.ser.is_open:
self.ser.close()
print(f"Serial port {self.port} closed.")
self.wait()
class MapView(QtWidgets.QGraphicsView):
def __init__(self, pixmap):
super().__init__()
self.scene = QtWidgets.QGraphicsScene()
self.setScene(self.scene)
self.pixmap_item = QtWidgets.QGraphicsPixmapItem(pixmap)
self.scene.addItem(self.pixmap_item)
self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform)
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
self.scale_factor = 1.0
def wheelEvent(self, event):
zoom_in_factor = 1.25
zoom_out_factor = 0.8
if event.angleDelta().y() > 0:
zoom_factor = zoom_in_factor
else:
zoom_factor = zoom_out_factor
self.scale(zoom_factor, zoom_factor)
self.scale_factor *= zoom_factor
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi("Ground_Station.ui", self)
self.image_path = "mapa.png"
self.tif_path = "mapa.tif"
self.base_pixmap = QtGui.QPixmap(self.image_path)
container = self.findChild(QtWidgets.QWidget, "map_container")
container_layout = QtWidgets.QVBoxLayout(container)
container_layout.setContentsMargins(0,0,0,0)
self.map_view = MapView(self.base_pixmap)
container_layout.addWidget(self.map_view)
try:
self.dataset = rasterio.open(self.tif_path)
except Exception as e:
QtWidgets.QMessageBox.critical(self, "Error", f"Cannot open TIFF:\n{e}")
sys.exit(1)
self.dst_crs = self.dataset.crs
self.points = []
self.serial_thread = SerialReader(port="COM7", baudrate=9600)
self.serial_thread.data_received.connect(self.process_serial_data)
self.serial_thread.start()
for widget in ["edit_lat", "edit_lon", "edit_alt", "edit_temp", "edit_rssi", "edit_snr"]:
getattr(self, widget).clear()
self.text_serial.clear()
@QtCore.pyqtSlot(float, float, float, float, float, float, str)
def process_serial_data(self, lat, lon, alt, temp, rssi, snr, full_line):
try:
self.text_serial.clear()
self.text_serial.append(full_line)
self.edit_lat.setText(f"{lat:.6f}")
self.edit_lon.setText(f"{lon:.6f}")
self.edit_alt.setText(f"{alt:.2f}")
self.edit_temp.setText(f"{temp:.2f}")
self.edit_rssi.setText(f"{rssi:.0f}")
self.edit_snr.setText(f"{snr:.1f}")
xs, ys = transform('EPSG:4326', self.dst_crs, [lon], [lat])
x, y = xs[0], ys[0]
row, col = self.dataset.index(x, y)
px, py = col, row
self.points.append((px, py))
self.update_map()
except Exception as e:
print(f"Error in process_serial_data: {e}")
traceback.print_exc()
def update_map(self):
try:
for item in self.map_view.scene.items():
if item != self.map_view.pixmap_item:
self.map_view.scene.removeItem(item)
for px, py in self.points:
ellipse = QtWidgets.QGraphicsEllipseItem(px-3, py-3, 6, 6)
ellipse.setBrush(QtCore.Qt.red)
ellipse.setPen(QtGui.QPen(QtCore.Qt.red, 1))
self.map_view.scene.addItem(ellipse)
except Exception as e:
print(f"Error updating map: {e}")
traceback.print_exc()
def closeEvent(self, event):
self.serial_thread.stop()
event.accept()
if __name__ == "__main__":
try:
app = QtWidgets.QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
except Exception as e:
print(f"Critical error: {e}")
import traceback
traceback.print_exc()
Reflections and improvements
- Developing EduCanSat was a transformative experience that expanded my skills in electronics, 3D design, programming, and project management. Facing unexpected problems and iterating solutions in real time deepened my understanding of system integration.
- This project required integrating knowledge from different fields—mechanical design, electronics, software, and communication systems. The process highlighted the value of a multidisciplinary approach in solving complex real-world challenges.
- Designing for reliability, ease of assembly, and robustness made me consider the end user at every stage. I learned to prioritize accessibility, especially for educational settings and communities with limited resources.
- Seeing EduCanSat in action with students and educators was extremely rewarding. The project not only demonstrates technical concepts but also inspires curiosity, teamwork, and problem-solving among participants.
- Documenting every step and making my work open-source was essential. It ensures that others can learn from, replicate, or even improve upon EduCanSat—amplifying its impact.
- I am motivated to keep improving EduCanSat by adding new sensors, expanding its educational applications, and collaborating with more communities and schools. I see great potential for this platform as a tool for hands-on STEM education.
Downloads
Here you can download all the source code files used in the EduCanSat project, including the CanSat firmware, the ground station firmware, and the Python visualization app.