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.

System Overview

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.

Case Design

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.

Packaging Inside

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.

Final Product Front

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.

Final Product Back

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.

Integration Diagram

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.

Python App Map

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

Flight Test

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.