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:


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

#endif

Secrets 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
TipTest with Power Profiling Tools Early

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

WarningPitfall: Requirements Scope Creep

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

WarningPitfall: Ignoring Non-Functional Requirements

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

Question: You’re prototyping a battery-powered IoT sensor that transmits data via LoRaWAN. Which development workflow would BEST optimize power consumption during the prototyping phase?

Explanation: Power optimization requires measurement-driven development. Power profilers reveal exactly where power is consumed. Random delay() calls don’t enable proper sleep modes. Arduino defaults keep everything running. Premature optimization without measurement wastes time on low-impact changes.

Question: Which THREE software approaches are MOST effective for reducing IoT device power consumption? (Select all that apply)

Explanation: B, D, E are correct. Deep sleep reduces 80mA to 10-100uA (800x improvement). Unused peripherals leak 1-10mA each. Duty cycling eliminates continuous sensor current. delay() keeps CPU running. Wi-Fi consumes 80-300mA. Higher frequency increases current.

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.