Practice Lab Guide

Project 4: Embedded Application with OOP Structure

Refactor procedural C++ telemetry code into a clean, object-oriented framework and deploy a Python serial interface.
Domain
Object-Oriented Firmware / Python
Difficulty
⭐⭐⭐☆☆ (Intermediate)
Course Module
Programming for IoT
Deliverables
C++ OOP Source Code, Python Serial Parser
1. Object-Oriented Firmware & Serial Integration Architecture

As embedded code grows, procedural structures become difficult to scale. Refactoring code into an Object-Oriented Programming (OOP) design isolates sensor details behind interfaces. The layout below details this architecture. The C++ firmware defines an abstract `Sensor` base class, implementing polymorphic inheritance to manage LDR and DHT22 subsystems. It serializes sensor data into a structured JSON string and transmits it over serial. In the VirtualBox Linux environment, a Python script reads the data from `/dev/ttyUSB0` and displays it in the console.

C++ CLASS POLYMORPHISM <<Abstract>> Sensor Base Class + virtual float read() = 0 DhtSensor - DHT _dhtObj + float read() override LdrSensor - int _analogPin + float read() override SERIAL DATA INGEST PIPELINE ESP32 Hardware JSON Stream USB @ 115200 pyserial /dev/ttyUSB0 Python JSON Console Output Parser parsedObj = json.loads(line) {"temp": 24.5, "light": 1024, "motion": false}
2. Part 1: Step-by-Step Individual Commands & Script Setup

Follow these detailed steps to build the object-oriented firmware, open the VirtualBox guest shell, install Python serial modules, and execute the reader stream.

STEP 1

Launch Terminal and Create Dedicated OOP Folder

Boot up your VirtualBox Ubuntu machine. Open the terminal (Ctrl+Alt+T) and create a directory to organize your C++ and Python source files.

ubuntu@iot-vm:~$ mkdir -p ~/workspace/oop_telemetry
We run `mkdir -p` to verify and generate a clean workspace directory tree at the path `~/workspace/oop_telemetry` without throwing errors if the parent directories already exist.
STEP 2

Navigate to the Project Folder

Move your current command shell context into the newly generated folder to manage files locally.

ubuntu@iot-vm:~$ cd ~/workspace/oop_telemetry
We execute `cd` to change the working directory path of our terminal process, ensuring all subsequent scripts and source code files are created inside the isolated oop_telemetry folder.
STEP 3

Generate Isolated Python Virtual Environment

Create a sandbox folder containing Python libraries to prevent script execution packages from conflicting with system libraries.

ubuntu@iot-vm:~/workspace/oop_telemetry$ python3 -m venv env
We run `python3 -m venv env` to create an isolated environment directory named `env` containing a copy of the Python interpreter, protecting our VM's global configuration.
STEP 4

Activate the Virtual Environment

Instruct the current shell to look for Python execution files inside the local project folder path instead of standard root directories.

ubuntu@iot-vm:~/workspace/oop_telemetry$ source env/bin/activate
We run the `source` command against the activation shell script, modifying terminal PATH variables so that running python packages uses the local virtual environment folders.
STEP 5

Install pyserial Module inside the Sandbox Environment

Install the Python serial communications library using pip. This package is required to connect to `/dev/ttyUSB0`.

(env) ubuntu@iot-vm:~/workspace/oop_telemetry$ pip install pyserial
We run `pip install` to download and install the `pyserial` module, enabling Python to configure serial port attributes, read byte streams, and handle serial connections.
STEP 6

Create the Python Serial Reader Script

Open the graphical text editor in the background to write the serial listener script.

(env) ubuntu@iot-vm:~/workspace/oop_telemetry$ gedit serial_reader.py &
We invoke `gedit` alongside the `&` symbol to open the graphical text editor in a background thread, leaving our terminal terminal prompt active for running commands.
STEP 7

Execute the Serial Reader Program

Run the Python reader. Make sure your ESP32 board is connected to the VM via the VirtualBox USB menu, and that the serial port is not open in the Arduino IDE serial monitor.

(env) ubuntu@iot-vm:~/workspace/oop_telemetry$ python3 serial_reader.py
We execute the Python reader script, starting the loop that polls `/dev/ttyUSB0`, decodes incoming JSON streams, and prints formatted telemetry to the console.
3. OOP Serialization & Parse Pipeline Flow

The OOP architecture manages telemetry data by wrapping hardware details inside classes. The workflow diagram below illustrates how values flow from raw analog signals, through the object interface, into a formatted JSON string, and finally to the parsed console dashboard.

1. OOP Abstraction Sensor* arr[2] Polymorphic read() Encapsulated offsets 2. JSON Serialization Serial.printf("{...}") Outputs raw ASCII Terminated with \n 3. Serial Listener sys/pyserial loop readline().decode() Strips whitespace 4. Console print json.loads() Formatted telemetry dashboard
4. Part 2: Complete Codebases & Line-by-Line Breakdowns

Below is the complete C++ firmware code showing how abstract inheritance interfaces work, followed by the complete Python script to listen to the serial port and print the data.

Asset 1: Polymorphic C++ Telemetry Code (`oop_telemetry.ino`)

Line-by-Line Code Breakdown

// Practice Project 4: C++ Object-Oriented Firmware Code #include <DHT.h> // Abstract Base Class Sensor class Sensor { public: virtual void begin() = 0; // Pure virtual setup method virtual float readVal() = 0; // Pure virtual read method }; // Subclass LdrSensor inheriting from Sensor class LdrSensor : public Sensor { private: int _pin; // Private GPIO pin reference public: LdrSensor(int pin) : _pin(pin) {} void begin() override { // ADC pins do not require explicit pinMode setup } float readVal() override { return (float)analogRead(_pin); // Return LDR voltage divider reading } }; // Subclass TempSensor inheriting from Sensor class TempSensor : public Sensor { private: int _pin; int _type; DHT _dht; // Encapsulate DHT object library public: TempSensor(int pin, int type) : _pin(pin), _type(type), _dht(pin, type) {} void begin() override { _dht.begin(); } float readVal() override { float t = _dht.readTemperature(); if (isnan(t)) { return -999.0; // Error indicator flag value } return t; } }; // Array of base class pointers Sensor* sensors[2]; void setup() { Serial.begin(115200); // Instantiate objects polymorphically sensors[0] = new TempSensor(15, DHT22); sensors[1] = new LdrSensor(34); // Initialize sensors using the interface for (int i = 0; i < 2; i++) { sensors[i]->begin(); } } void loop() { // Fetch measurements float temperature = sensors[0]->readVal(); float lightValue = sensors[1]->readVal(); // Print measurements as a structured JSON string Serial.print("{\"temperature\":"); Serial.print(temperature); Serial.print(",\"light\":"); Serial.print(lightValue); Serial.println("}"); delay(2000); }

Asset 2: Python Serial Ingestion Script (`serial_reader.py`)

Line-by-Line Code Breakdown

# Practice Project 4: Python Serial Listener Script import serial import json import sys def main(): # Target port on VirtualBox Ubuntu VM port_path = '/dev/ttyUSB0' baud_rate = 115200 print(f"Connecting to microcontroller serial interface on {port_path}...") try: # Initialize the serial connection object ser = serial.Serial(port=port_path, baudrate=baud_rate, timeout=1.0) # Flush buffers to ignore incomplete initial readings ser.reset_input_buffer() print("Connection established. Waiting for JSON telemetry...") print("==================================================") except serial.SerialException as e: print(f"Error: Could not open port {port_path}. details: {e}") print("Suggestions: Check VirtualBox USB settings or close other Serial Monitors.") sys.exit(1) try: while True: # Read until a newline character '\n' is found raw_line = ser.readline() if raw_line: try: # Decode bytes to string and strip whitespaces decoded_line = raw_line.decode('utf-8').strip() # Parse the JSON string telemetry = json.loads(decoded_line) # Print the formatted readings to the terminal temp = telemetry.get('temperature', 'N/A') light = telemetry.get('light', 'N/A') print(f"[TELEMETRY] Temperature: {temp:.2f} C | Light Sensor: {light:.0f}") except UnicodeDecodeError: print("[WARN] Received malformed serial bytes. Skipping...") except json.JSONDecodeError: # Handle non-JSON debug strings gracefully if len(decoded_line) > 0: print(f"[DEBUG] {decoded_line}") except KeyboardInterrupt: print("\nClosing serial port connection...") ser.close() print("Exited.") if __name__ == "__main__": main()
5. Deliverables Summary

Created Artifacts

  • Refactored polymorphic C++ project sketch code: oop_telemetry.ino.
  • Python script for serial data parsing: serial_reader.py.

Verification Proof

  • A compilation report showing 0 memory leaks and clean variable scopes.
  • A screenshot of the Ubuntu terminal running serial_reader.py, displaying real-time telemetry data parsed from the serial port.
6. Closing Explanation: Why We Did This & What It Accomplishes

Architectural Intent & Operational Impact

Why We Did This

What This Accomplishes