cpp
esp32
arduino
programming
C++ Primer for ESP32
A beginner-friendly guide to understanding the TSI-Telemetry firmware code.
File Structure
firmware/tsi/
├── tsi.ino # Main entry point (like main() in C)
├── Config.h # Constants and settings
├── CarData.h # Data structure for telemetry
├── PIDCommands.h # OBD-II PID definitions
├── BLEManager.h/cpp # Bluetooth handling
├── OBDParser.h/cpp # Response parsing
├── DisplayManager.h/cpp # Serial output
├── WiFiManager.h/cpp # WiFi connection
└── MQTTManager.h/cpp # MQTT publishing
Header (.h) vs Implementation (.cpp)
Header file - declares what exists (the "menu"):
// WiFiManager.h
class WiFiManager {
bool connect(); // "There's a function called connect"
bool isConnected(); // "There's a function called isConnected"
};
Implementation file - defines how it works (the "kitchen"):
// WiFiManager.cpp
bool WiFiManager::connect() {
// Actual code that connects to WiFi
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
// ...
}
Include Guards
Every header file has this pattern:
#ifndef CARDATA_H // If CARDATA_H is not defined...
#define CARDATA_H // Define it now
// ... actual code ...
#endif // End the conditional
Why? Prevents the same code from being included twice, which would cause "redefinition" errors.
Data Types
| Type | What it holds | Example |
|---|---|---|
int |
Whole numbers (-2B to +2B) | int rpm = 2500; |
float |
Decimals | float voltage = 14.2; |
bool |
true or false | bool connected = true; |
String |
Text (Arduino) | String name = "ESP32"; |
char |
Single character | char letter = 'A'; |
unsigned long |
Big positive numbers | unsigned long time = millis(); |
Structs - Grouping Related Data
struct CarData {
int rpm = 0; // Member variable with default
int speed = 0;
float batteryVoltage = 0.0;
void reset() { // Member function
rpm = 0;
speed = 0;
batteryVoltage = 0.0;
}
};
// Usage:
CarData carData; // Create instance
carData.rpm = 2500; // Access with dot
carData.reset(); // Call function
Classes - Structs with Privacy
class WiFiManager {
public: // Anyone can access
WiFiManager(); // Constructor
bool connect();
bool isConnected();
private: // Only class can access
unsigned long lastConnectAttempt;
};
public vs private:
public: Like a car's steering wheel - anyone can useprivate: Like the engine internals - hidden implementation
Constructors
Special function that runs when object is created:
// Declaration
WiFiManager();
// Implementation with initializer list
WiFiManager::WiFiManager() : lastConnectAttempt(0) {}
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// Sets lastConnectAttempt to 0
The :: Operator (Scope Resolution)
bool WiFiManager::connect() {
// ^ ^
// | |
// class name function name
// "connect() belongs to WiFiManager class"
}
Pointers (The Tricky Part)
A pointer holds a memory address, not a value:
int x = 5; // Variable holds value 5
int* ptr = &x; // Pointer holds address of x
// & = "address of"
Serial.println(x); // Prints: 5
Serial.println(*ptr); // Prints: 5 (* = "value at address")
Why use pointers?
// Without pointer - makes a COPY (bad for large objects)
void updateRPM(CarData data) {
data.rpm = 2500; // Only updates the copy!
}
// With pointer - modifies ORIGINAL
void updateRPM(CarData* data) {
data->rpm = 2500; // Updates the real thing!
}
// ^
// Arrow operator: access through pointer
Arduino Program Structure
Every Arduino sketch needs two functions:
void setup() {
// Runs ONCE when device boots
Serial.begin(115200);
WiFi.begin(ssid, password);
}
void loop() {
// Runs FOREVER, repeatedly
readSensors();
publishData();
delay(1000);
}
Control Flow
If/Else
if (WiFi.status() == WL_CONNECTED) {
Serial.println("Connected!");
} else {
Serial.println("Not connected");
}
While Loop
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
For Loop
for (int i = 0; i < 10; i++) {
Serial.println(i);
}
Functions
// Function declaration
bool connect(); // Returns true/false
void printStatus(); // Returns nothing (void)
int getRPM(); // Returns integer
String getIP(); // Returns String
// Function with parameters
void setSpeed(int speed) {
carData.speed = speed;
}
// Function with pointer parameter
void parseData(CarData* data) {
data->rpm = 2500;
}
Common Arduino Functions
| Function | Purpose |
|---|---|
Serial.begin(115200) |
Start serial at baud rate |
Serial.println("text") |
Print with newline |
Serial.printf("%d", val) |
Formatted print |
delay(1000) |
Wait 1000 milliseconds |
millis() |
Get milliseconds since boot |
pinMode(pin, OUTPUT) |
Set pin as output |
digitalWrite(pin, HIGH) |
Set pin high |
String vs char[]
// Arduino String (easy, but uses more memory)
String name = "ESP32";
name += "-car"; // Concatenation
// C-style char array (efficient, harder)
char name[] = "ESP32";
// Can't easily concatenate
For ESP32 with limited memory, char[] is better for fixed strings, String is okay for dynamic content.
Memory Considerations
ESP32 has limited RAM (320KB). Watch out for:
// Bad - creates many String copies
String json = "{";
json += "\"rpm\":" + String(rpm) + ",";
json += "\"speed\":" + String(speed);
// Each += creates a new String!
// Better - use sprintf
char json[256];
sprintf(json, "{\"rpm\":%d,\"speed\":%d}", rpm, speed);
Debugging Tips
// Print to serial monitor
Serial.println("Got here!");
Serial.printf("RPM = %d\n", rpm);
// Check memory
Serial.printf("Free heap: %d\n", ESP.getFreeHeap());
// Print pointer address
Serial.printf("Address: %p\n", &carData);
Common Errors
| Error | Meaning | Fix |
|---|---|---|
undefined reference |
Function declared but not implemented | Add .cpp implementation |
redefinition of |
Same thing defined twice | Add include guards |
no matching function |
Wrong parameters | Check function signature |
cannot convert |
Type mismatch | Cast or change type |