Skip to content

Advanced Usage Example

Advanced techniques including error handling, filtering, and optimization.

Overview

This example demonstrates advanced features:

  • Robust error handling
  • Data filtering (median, moving average)
  • Performance optimization
  • State management
  • Diagnostic tools

Hardware Required

  • Arduino Uno (or compatible)
  • HC-SR04 Ultrasonic Sensor
  • Jumper wires
  • LED (optional, for status indication)

Complete Advanced Example

cpp
/**
 * Advanced Usage Example - MinimalUltrasonic
 * 
 * Demonstrates:
 * - Median filtering for stability
 * - Error handling and recovery
 * - Performance monitoring
 * - State management
 * - Diagnostic output
 */

#include <MinimalUltrasonic.h>

// Configuration
const uint8_t TRIG_PIN = 12;
const uint8_t ECHO_PIN = 13;
const uint8_t LED_PIN = LED_BUILTIN;

// Thresholds
const float MIN_VALID_DISTANCE = 2.0;    // cm
const float MAX_VALID_DISTANCE = 350.0;  // cm
const float THRESHOLD_NEAR = 30.0;       // cm
const float THRESHOLD_FAR = 200.0;       // cm

// Create sensor
MinimalUltrasonic sensor(TRIG_PIN, ECHO_PIN);

// State tracking
enum State {
  STATE_UNKNOWN,
  STATE_TOO_CLOSE,
  STATE_NEAR,
  STATE_NORMAL,
  STATE_FAR,
  STATE_NO_OBJECT
};

State currentState = STATE_UNKNOWN;
State previousState = STATE_UNKNOWN;

// Statistics
struct Statistics {
  unsigned long totalReadings;
  unsigned long validReadings;
  unsigned long errorReadings;
  float minDistance;
  float maxDistance;
  float avgDistance;
  unsigned long startTime;
};

Statistics stats = {0, 0, 0, 999999.0, 0, 0, 0};

// Filtering
const int FILTER_SIZE = 5;
float filterBuffer[FILTER_SIZE];
int filterIndex = 0;
bool filterFilled = false;

/**
 * Get median filtered distance
 */
float getMedianDistance() {
  float readings[FILTER_SIZE];
  int validCount = 0;
  
  // Collect readings
  for (int i = 0; i < FILTER_SIZE; i++) {
    float dist = sensor.read();
    
    if (dist > 0) {
      readings[validCount++] = dist;
    }
    
    delay(20);
  }
  
  // Need at least 3 valid readings
  if (validCount < 3) {
    return 0;
  }
  
  // Sort readings (bubble sort)
  for (int i = 0; i < validCount - 1; i++) {
    for (int j = i + 1; j < validCount; j++) {
      if (readings[i] > readings[j]) {
        float temp = readings[i];
        readings[i] = readings[j];
        readings[j] = temp;
      }
    }
  }
  
  // Return median
  return readings[validCount / 2];
}

/**
 * Get moving average distance
 */
float getMovingAverage() {
  float distance = sensor.read();
  
  if (distance > 0) {
    filterBuffer[filterIndex] = distance;
    filterIndex = (filterIndex + 1) % FILTER_SIZE;
    
    if (filterIndex == 0) {
      filterFilled = true;
    }
  }
  
  // Calculate average
  int count = filterFilled ? FILTER_SIZE : filterIndex;
  if (count == 0) return 0;
  
  float sum = 0;
  for (int i = 0; i < count; i++) {
    sum += filterBuffer[i];
  }
  
  return sum / count;
}

/**
 * Update statistics
 */
void updateStatistics(float distance) {
  stats.totalReadings++;
  
  if (distance > 0) {
    stats.validReadings++;
    
    // Update min/max
    if (distance < stats.minDistance) {
      stats.minDistance = distance;
    }
    if (distance > stats.maxDistance) {
      stats.maxDistance = distance;
    }
    
    // Update running average
    stats.avgDistance = 
      (stats.avgDistance * (stats.validReadings - 1) + distance) / 
      stats.validReadings;
  } else {
    stats.errorReadings++;
  }
}

/**
 * Determine state from distance
 */
State determineState(float distance) {
  if (distance == 0) {
    return STATE_NO_OBJECT;
  }
  
  if (distance < MIN_VALID_DISTANCE) {
    return STATE_TOO_CLOSE;
  }
  
  if (distance < THRESHOLD_NEAR) {
    return STATE_NEAR;
  }
  
  if (distance < THRESHOLD_FAR) {
    return STATE_NORMAL;
  }
  
  if (distance <= MAX_VALID_DISTANCE) {
    return STATE_FAR;
  }
  
  return STATE_NO_OBJECT;
}

/**
 * Get state name
 */
const char* getStateName(State state) {
  switch (state) {
    case STATE_TOO_CLOSE: return "TOO CLOSE";
    case STATE_NEAR: return "NEAR";
    case STATE_NORMAL: return "NORMAL";
    case STATE_FAR: return "FAR";
    case STATE_NO_OBJECT: return "NO OBJECT";
    default: return "UNKNOWN";
  }
}

/**
 * Handle state change
 */
void onStateChange(State oldState, State newState) {
  Serial.println();
  Serial.print("State changed: ");
  Serial.print(getStateName(oldState));
  Serial.print(" -> ");
  Serial.println(getStateName(newState));
  
  // Visual indication
  if (newState == STATE_NEAR || newState == STATE_TOO_CLOSE) {
    digitalWrite(LED_PIN, HIGH);
  } else {
    digitalWrite(LED_PIN, LOW);
  }
}

/**
 * Print statistics
 */
void printStatistics() {
  float successRate = 
    (stats.totalReadings > 0) ? 
    (float)stats.validReadings / stats.totalReadings * 100.0 : 0;
  
  unsigned long elapsed = (millis() - stats.startTime) / 1000;
  
  Serial.println();
  Serial.println("=== Statistics ===");
  Serial.print("Runtime: ");
  Serial.print(elapsed);
  Serial.println(" seconds");
  
  Serial.print("Total readings: ");
  Serial.println(stats.totalReadings);
  
  Serial.print("Valid: ");
  Serial.print(stats.validReadings);
  Serial.print(" (");
  Serial.print(successRate, 1);
  Serial.println("%)");
  
  Serial.print("Errors: ");
  Serial.println(stats.errorReadings);
  
  if (stats.validReadings > 0) {
    Serial.print("Min distance: ");
    Serial.print(stats.minDistance, 1);
    Serial.println(" cm");
    
    Serial.print("Max distance: ");
    Serial.print(stats.maxDistance, 1);
    Serial.println(" cm");
    
    Serial.print("Avg distance: ");
    Serial.print(stats.avgDistance, 1);
    Serial.println(" cm");
  }
  
  Serial.println("==================");
  Serial.println();
}

/**
 * Diagnostic check
 */
void runDiagnostics() {
  Serial.println("Running diagnostics...");
  Serial.println();
  
  // Test configuration
  Serial.println("Configuration:");
  Serial.print("  Timeout: ");
  Serial.print(sensor.getTimeout());
  Serial.println(" µs");
  
  float maxRange = sensor.getTimeout() / 58.8235;
  Serial.print("  Max range: ");
  Serial.print(maxRange, 1);
  Serial.println(" cm");
  
  Serial.print("  Unit: ");
  switch (sensor.getUnit()) {
    case MinimalUltrasonic::CM: Serial.println("CM"); break;
    case MinimalUltrasonic::METERS: Serial.println("METERS"); break;
    case MinimalUltrasonic::MM: Serial.println("MM"); break;
    case MinimalUltrasonic::INCHES: Serial.println("INCHES"); break;
    default: Serial.println("Other");
  }
  
  Serial.println();
  
  // Test readings
  Serial.println("Testing 10 readings:");
  int successCount = 0;
  
  for (int i = 0; i < 10; i++) {
    float dist = sensor.read();
    
    Serial.print("  ");
    Serial.print(i + 1);
    Serial.print(". ");
    
    if (dist > 0) {
      Serial.print("✓ ");
      Serial.print(dist, 1);
      Serial.println(" cm");
      successCount++;
    } else {
      Serial.println("✗ Error");
    }
    
    delay(100);
  }
  
  Serial.println();
  Serial.print("Success rate: ");
  Serial.print(successCount);
  Serial.println("/10");
  
  if (successCount >= 8) {
    Serial.println("✓ Sensor operating normally");
  } else if (successCount >= 5) {
    Serial.println("⚠️  Sensor may have issues");
  } else {
    Serial.println("❌ Sensor appears faulty");
  }
  
  Serial.println();
}

void setup() {
  Serial.begin(9600);
  pinMode(LED_PIN, OUTPUT);
  
  Serial.println("MinimalUltrasonic - Advanced Example");
  Serial.println("====================================");
  Serial.println();
  
  // Configure sensor
  sensor.setUnit(MinimalUltrasonic::CM);
  sensor.setTimeout(20000UL);
  
  // Run diagnostics
  runDiagnostics();
  
  // Initialize statistics
  stats.startTime = millis();
  
  Serial.println("Starting monitoring...");
  Serial.println("Type 's' for statistics");
  Serial.println();
}

void loop() {
  // Get filtered distance
  float distance = getMedianDistance();
  
  // Update statistics
  updateStatistics(distance);
  
  // Determine state
  currentState = determineState(distance);
  
  // Handle state change
  if (currentState != previousState) {
    onStateChange(previousState, currentState);
  }
  previousState = currentState;
  
  // Display reading
  Serial.print(getStateName(currentState));
  Serial.print(": ");
  
  if (distance > 0) {
    Serial.print(distance, 1);
    Serial.println(" cm");
  } else {
    Serial.println("---");
  }
  
  // Check for statistics request
  if (Serial.available()) {
    char cmd = Serial.read();
    if (cmd == 's' || cmd == 'S') {
      printStatistics();
    }
  }
  
  delay(500);
}

Key Features Explained

1. Median Filtering

Removes outliers and provides stable readings:

cpp
float getMedianDistance() {
  // Take multiple readings
  // Sort them
  // Return middle value
  // More stable than single reading
}

Benefits:

  • Removes outliers
  • More reliable than single reading
  • Good for noisy environments

Trade-off:

  • Slower (5 readings)
  • More code

2. Moving Average

Smooth out variations:

cpp
float getMovingAverage() {
  // Maintain circular buffer
  // Calculate average of last N readings
  // Smooth, continuous values
}

Benefits:

  • Smooth transitions
  • Less CPU than median
  • Continuous updates

Trade-off:

  • Lags behind changes
  • Needs initialization time

3. State Management

Track system state:

cpp
enum State {
  STATE_TOO_CLOSE,
  STATE_NEAR,
  STATE_NORMAL,
  STATE_FAR,
  STATE_NO_OBJECT
};

Benefits:

  • Clear system state
  • Easy state change detection
  • Organized logic

4. Statistics Tracking

Monitor performance:

cpp
struct Statistics {
  unsigned long totalReadings;
  unsigned long validReadings;
  unsigned long errorReadings;
  // etc.
};

Benefits:

  • Track success rate
  • Identify issues
  • Performance monitoring

5. Error Handling

Robust error management:

cpp
if (distance == 0) {
  // Handle error
  stats.errorReadings++;
  return STATE_NO_OBJECT;
}

// Validate range
if (distance < MIN_VALID || distance > MAX_VALID) {
  // Out of valid range
}

Advanced Techniques

Exponential Moving Average

cpp
float ema = 0;
const float ALPHA = 0.3;

float getEMA(float newValue) {
  if (ema == 0) {
    ema = newValue;
  } else {
    ema = ALPHA * newValue + (1 - ALPHA) * ema;
  }
  return ema;
}

Outlier Rejection

cpp
bool isOutlier(float value, float mean, float threshold) {
  return abs(value - mean) > threshold;
}

float getFilteredReading() {
  float current = sensor.read();
  
  if (isOutlier(current, stats.avgDistance, 50.0)) {
    // Reject outlier, use last valid
    return lastValidDistance;
  }
  
  return current;
}

Hysteresis

Prevent state oscillation:

cpp
const float HYSTERESIS = 5.0;  // 5cm hysteresis

State getStateWithHysteresis(float distance) {
  if (currentState == STATE_NEAR && distance < THRESHOLD_NEAR + HYSTERESIS) {
    return STATE_NEAR;  // Stay in state
  }
  
  // Normal state determination
  return determineState(distance);
}

Performance Optimization

Fast Mode (No Filtering)

cpp
void fastLoop() {
  float distance = sensor.read();  // Single reading
  
  if (distance > 0) {
    processDistance(distance);
  }
  
  delay(60);  // Minimum delay
}

Balanced Mode (Light Filtering)

cpp
void balancedLoop() {
  float d1 = sensor.read();
  delay(30);
  float d2 = sensor.read();
  
  float distance = (d1 + d2) / 2.0;  // Simple average
  processDistance(distance);
  
  delay(100);
}

Accurate Mode (Heavy Filtering)

cpp
void accurateLoop() {
  float distance = getMedianDistance();  // Median of 5
  processDistance(distance);
  
  delay(200);
}

Debugging Tools

Verbose Output

cpp
void printDetailed(float distance) {
  Serial.print("[");
  Serial.print(millis());
  Serial.print("] ");
  Serial.print("Distance: ");
  Serial.print(distance, 2);
  Serial.print(" cm | State: ");
  Serial.print(getStateName(currentState));
  Serial.print(" | Valid: ");
  Serial.print(stats.validReadings);
  Serial.print("/");
  Serial.println(stats.totalReadings);
}

Plot-Friendly Output

cpp
void printForPlotter() {
  float distance = sensor.read();
  
  // Format for Serial Plotter
  Serial.print("Distance:");
  Serial.print(distance);
  Serial.print(",Threshold:");
  Serial.println(THRESHOLD_NEAR);
}

Production-Ready Code

Optimized for deployment:

cpp
// Remove Serial prints
// Add only essential error handling
// Use fastest filtering that meets requirements
// Optimize delays
// Add watchdog timer
// Implement power saving if needed

Next Steps

  • Study each technique individually
  • Combine techniques for your needs
  • Test in actual deployment environment
  • Profile performance
  • Optimize based on measurements

See Also

Released under the MIT License.