1535 Software Prototyping: Best Practices and Common Pitfalls
1535.1 Learning Objectives
By the end of this chapter, you will be able to:
- Organize Code Effectively: Structure firmware into modular, maintainable components
- Manage Configuration: Separate secrets from code and enable runtime configuration
- Optimize Power Consumption: Implement sleep modes and duty cycling
- Handle Errors Gracefully: Build robust error recovery and watchdog protection
- Avoid Common Mistakes: Recognize and prevent the most frequent IoT firmware bugs
1535.2 Prerequisites
Before diving into this chapter, you should be familiar with:
- Testing and Debugging: Debugging techniques and testing approaches
- Software Architecture Patterns: Firmware organization patterns
1535.3 Code Organization
Modular Structure:
// sensors.h
#ifndef SENSORS_H
#define SENSORS_H
void initSensors();
float readTemperature();
float readHumidity();
#endif
// sensors.cpp
#include "sensors.h"
#include <Adafruit_BME280.h>
Adafruit_BME280 bme;
void initSensors() {
bme.begin(0x76);
}
float readTemperature() {
return bme.readTemperature();
}Benefits: - Reusable components - Easier testing - Clearer dependencies - Maintainable codebase
1535.4 Configuration Management
Centralized Configuration:
// config.h
#ifndef CONFIG_H
#define CONFIG_H
// Wi-Fi Configuration
#define WIFI_SSID "YourSSID"
#define WIFI_PASSWORD "YourPassword"
// MQTT Configuration
#define MQTT_SERVER "mqtt.example.com"
#define MQTT_PORT 1883
// Sensor Configuration
#define SENSOR_READ_INTERVAL 60000 // ms
#define TEMPERATURE_OFFSET 0.0
#endifSecrets Management:
// secrets.h (add to .gitignore)
#define WIFI_PASSWORD "actual_password"
#define API_KEY "actual_api_key"
// secrets.h.template (commit to repo)
#define WIFI_PASSWORD "your_password_here"
#define API_KEY "your_api_key_here"Runtime Configuration (EEPROM/NVS):
#include <Preferences.h>
Preferences prefs;
void loadConfig() {
prefs.begin("config", true); // Read-only
String ssid = prefs.getString("wifi_ssid", "");
String password = prefs.getString("wifi_pass", "");
prefs.end();
}
void saveConfig(String ssid, String password) {
prefs.begin("config", false); // Read-write
prefs.putString("wifi_ssid", ssid);
prefs.putString("wifi_pass", password);
prefs.end();
}1535.5 Power Management
Sleep Modes:
#include <esp_sleep.h>
void enterDeepSleep(int seconds) {
esp_sleep_enable_timer_wakeup(seconds * 1000000ULL);
esp_deep_sleep_start();
}
void loop() {
readSensorsAndSend();
enterDeepSleep(300); // Sleep 5 minutes
}Peripheral Power Down:
void powerDown() {
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
btStop();
// Disable unused peripherals
}Power Budget Calculation:
Scenario: Read sensor every 60 seconds
Without deep sleep (modem sleep):
Power: 25 mA continuous
Battery (2000 mAh): 2000 / 25 = 80 hours = 3.3 days
With deep sleep:
Wake cycle: 300 ms x 100 mA = 8.33 uAh
Sleep: 59.7 s x 0.01 mA = 0.166 uAh
Per minute: 8.5 uAh
Battery (2000 mAh): 2000 / 0.0085 = 235,294 minutes = 163 days!
Improvement: 49x longer battery life
A critical oversight is developing firmware without measuring actual power consumption until deployment. Prototype testing on USB power masks the reality of battery operation.
Example: A “low power” environmental sensor prototype ran fine on USB for months, but deployed units with 2000mAh batteries lasted only 3 days instead of projected 6 months - Wi-Fi wasn’t properly sleeping, consuming 80mA continuously.
Solution: Use power profilers (Nordic Power Profiler Kit, Joulescope, or simple INA219 breakout) during development. Measure current in all states: active, idle, sleep, transmission.
1535.6 Error Handling
Graceful Degradation:
bool sendData() {
int retries = 3;
while(retries > 0) {
if (WiFi.status() == WL_CONNECTED) {
if (client.publish(topic, data)) {
return true;
}
}
retries--;
delay(1000);
}
// Store data locally for later transmission
storeDataLocally(data);
return false;
}Watchdog Timer:
#include <esp_task_wdt.h>
void setup() {
esp_task_wdt_init(30, true); // 30 second watchdog
esp_task_wdt_add(NULL);
}
void loop() {
// Feed watchdog to prevent reset
esp_task_wdt_reset();
// Your code
doWork();
}1535.7 Memory Management
Avoid Memory Leaks:
// Bad: Memory leak
void loop() {
char* buffer = new char[1024];
processData(buffer);
// Missing: delete[] buffer;
}
// Good: Proper cleanup
void loop() {
char* buffer = new char[1024];
processData(buffer);
delete[] buffer;
}
// Better: Stack allocation
void loop() {
char buffer[1024];
processData(buffer);
// Automatically freed
}Monitor Memory Usage:
void printMemoryUsage() {
Serial.print("Free heap: ");
Serial.println(ESP.getFreeHeap());
Serial.print("Heap fragmentation: ");
Serial.println(ESP.getHeapFragmentation());
}1535.8 Common Pitfalls and Solutions
1535.8.1 Pitfall 1: Blocking Code
Problem:
// BAD: This blocks for 5 seconds!
void loop() {
delay(5000); // Device can't respond to anything
readSensor();
}Solution:
// GOOD: Non-blocking delay
unsigned long lastRead = 0;
void loop() {
if (millis() - lastRead >= 5000) {
readSensor();
lastRead = millis();
}
// Can do other things while "waiting"
}1535.8.2 Pitfall 2: No Error Handling
Problem:
// BAD: Assumes everything always works
void loop() {
float temp = dht.readTemperature();
sendToCloud(temp);
}Solution:
// GOOD: Always check return values
void loop() {
float temp = dht.readTemperature();
if (isnan(temp)) {
Serial.println("ERROR: Sensor read failed!");
return;
}
if (WiFi.status() != WL_CONNECTED) {
saveToLocalStorage(temp);
return;
}
if (!sendToCloud(temp)) {
retryQueue.add(temp);
}
}1535.8.3 Pitfall 3: Hardcoded Credentials
Problem:
// BAD: Credentials visible in code!
const char* ssid = "MyHomeWiFi";
const char* password = "MyPassword123";
const char* apiKey = "sk_live_51ABC123...";Why it’s bad: - Security risk: Anyone with your code has your passwords - Can’t change: Different Wi-Fi at customer site requires recompiling - GitHub leak: Accidentally commit to public repo -> API key stolen
Solution:
// GOOD: Store in EEPROM, configure via web interface
void setup() {
preferences.begin("wifi", true);
String ssid = preferences.getString("ssid", "");
String password = preferences.getString("password", "");
preferences.end();
if (ssid.length() == 0) {
startConfigPortal(); // Let user enter credentials
}
}1535.8.4 Pitfall 4: Interrupt Safety
Problem:
// BAD: Race condition!
int counter = 0;
void IRAM_ATTR buttonISR() {
counter++;
}
void loop() {
Serial.println(counter);
}Solution:
// GOOD: Volatile and atomic access
volatile int counter = 0;
void IRAM_ATTR buttonISR() {
counter++;
}
void loop() {
noInterrupts();
int localCounter = counter;
interrupts();
Serial.println(localCounter);
}1535.8.5 Pitfall 5: No Watchdog Timer
Problem:
// Prototype runs fine, then hangs forever
void loop() {
readSensor();
sendToCloud(); // If this hangs, device freezes!
}Real consequence: 500 environmental sensors deployed in remote rainforest. 100 froze within first week. No physical access to restart. Had to send technicians on 6-hour hike ($15,000 cost).
Solution:
#include <esp_task_wdt.h>
void setup() {
esp_task_wdt_init(30, true); // 30-second watchdog
esp_task_wdt_add(NULL);
}
void loop() {
esp_task_wdt_reset(); // Pet the dog every loop
readSensor();
sendToCloud();
// If loop hangs for 30+ seconds, watchdog resets device
}1535.9 Requirements Pitfalls
The mistake: Continuously adding features during prototyping without re-evaluating timeline, budget, or feasibility.
Symptoms: - Feature list grows after every stakeholder meeting - Original 3-month timeline becomes 9 months - Team working on multiple half-finished features simultaneously
The fix: - Define MVP before prototyping starts - Create a “feature parking lot” for post-MVP ideas - Require trade-off analysis: “What do we cut to add this?” - Use timeboxing: fixed end dates, not feature gates
The mistake: Focusing entirely on features while ignoring security, power consumption, reliability.
Symptoms: - Prototype sends data over unencrypted HTTP - Battery life measured in hours instead of months - Device crashes after 24 hours due to memory leaks - No OTA update mechanism
The fix: - Security: Use TLS from the first prototype - Power: Measure current consumption weekly - Reliability: Implement watchdog timers in prototype code - Maintainability: Design OTA mechanism before field deployment
1535.10 Summary Table
| Mistake | Symptom | Fix |
|---|---|---|
| Blocking code | Device unresponsive | Use millis() instead of delay() |
| No error handling | Random crashes | Check all return values |
| Hardcoded credentials | Can’t deploy to different sites | Store in EEPROM, use config portal |
| Memory leaks | Crashes after hours/days | Free allocated memory, use stack |
| Not testing edge cases | Field failures | Test Wi-Fi drops, sensor errors |
| Copy-paste code | Security/reliability issues | Understand and validate all code |
| No watchdog timer | Freezes require manual restart | Enable hardware watchdog |
Golden Rule: Every “I’ll fix it later” in your prototype becomes a $10,000 bug in production. Fix it NOW while it’s easy!
1535.11 Knowledge Check
1535.12 What’s Next
You have completed the Software Prototyping chapter series! Return to the Software Prototyping Overview to review all topics, or continue to Energy-Aware Considerations for a deeper dive into power optimization strategies.