Chapter 17: Integration & Deployment
Learning Objectives
This chapter covers:
- Integrate all components into a complete temperature monitoring system
- Configure build optimization for embedded deployment
- Flash and debug applications on ESP32-C3 hardware
- Implement basic error handling and recovery
Task: Build Production-Ready Temperature Monitor
Over chapters 13-16, we’ve built individual components. Now it’s time to integrate everything into a robust, production-ready system.
Your Mission:
- Integrate all components into a single working system
- Add error handling and recovery mechanisms
- Optimize build configuration for production deployment
- Add deployment scripts for easy flashing and monitoring
- Create production monitoring with structured output
What We’re Combining:
- Chapter 13: Hardware interaction with ESP32-C3 and temperature sensor
- Chapter 14: Embedded data structures with no_std foundations
- Chapter 15: Comprehensive testing strategy for embedded code
- Chapter 16: JSON communication and structured data protocols
Production Requirements:
- Graceful error handling (no panics in production)
- Optimized binary size and performance
- Reliable sensor reading with fallback
- Structured logging for monitoring
- Easy deployment and debugging
Simplified System Architecture
┌─────────────────────────────────────────────────────────┐
│ ESP32-C3 System │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Main Loop │ │
│ │ │ │
│ │ 1. Read Temperature │ │
│ │ 2. Store in Buffer │ │
│ │ 3. Update LED Status │ │
│ │ 4. Output JSON (every 5 readings) │ │
│ │ 5. Delay 1 second │ │
│ │ 6. Repeat │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Temperature │ │ LED │ │ JSON │ │
│ │Buffer │ │ Controller │ │ Output │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ USB Serial Output │ │
│ │ Status Messages | Readings | Statistics │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Complete Temperature Monitor Implementation
Project Setup
First, let’s create the complete Cargo.toml:
[package]
name = "chapter17_integration"
version = "0.1.0"
edition = "2024"
rust-version = "1.88"
[[bin]]
name = "chapter17_integration"
path = "./src/bin/main.rs"
[lib]
name = "chapter17_integration"
path = "src/lib.rs"
[dependencies]
# Only include ESP dependencies when not testing
esp-hal = { version = "1.0.0", features = ["esp32c3", "unstable"], optional = true }
esp-bootloader-esp-idf = { version = "0.4.0", features = ["esp32c3"], optional = true }
esp-println = { version = "0.16", features = ["esp32c3"], optional = true }
# Core dependencies
heapless = "0.8"
# Serialization
serde = { version = "1.0", default-features = false, features = ["derive"] }
serde-json-core = "0.6"
[features]
default = ["hardware"]
hardware = ["esp-hal", "esp-println", "esp-bootloader-esp-idf"]
simulation = [] # Use mock sensors instead of hardware
verbose = [] # Enable detailed debug logging
telemetry = [] # Extended monitoring capabilities
[profile.dev]
# Rust debug is too slow for embedded
opt-level = "s"
[profile.release]
# Production optimizations
codegen-units = 1 # LLVM can perform better optimizations using a single thread
debug = 2
debug-assertions = false
incremental = false
lto = 'fat'
opt-level = 's'
overflow-checks = false
Main System Implementation
// src/bin/main.rs - Production-ready integrated system #![no_std] #![no_main] #![deny( clippy::mem_forget, reason = "mem::forget is generally not safe to do with esp_hal types" )] use esp_hal::clock::CpuClock; use esp_hal::gpio::{Level, Output, OutputConfig}; use esp_hal::main; use esp_hal::time::{Duration, Instant}; use esp_hal::tsens::{Config, TemperatureSensor}; // Use the integrated system components from previous chapters use chapter17_integration::{Temperature, TemperatureBuffer, Command, TemperatureComm}; // Production system configuration const BUFFER_SIZE: usize = 32; const SAMPLE_RATE_MS: u32 = 1000; const JSON_OUTPUT_INTERVAL: u32 = 5; const HEALTH_REPORT_INTERVAL: u32 = 20; // System state tracking for production monitoring struct SystemState { reading_count: u32, system_time_ms: u32, overheating_count: u32, sensor_error_count: u32, last_temp: f32, } impl SystemState { fn new() -> Self { Self { reading_count: 0, system_time_ms: 0, overheating_count: 0, sensor_error_count: 0, last_temp: 0.0, } } fn advance_time(&mut self) { self.reading_count += 1; self.system_time_ms += SAMPLE_RATE_MS; } } #[panic_handler] fn panic(_: &core::panic::PanicInfo) -> ! { // In production, we want graceful error handling esp_println::println!("SYSTEM_ERROR: Panic occurred, attempting recovery..."); loop {} } esp_bootloader_esp_idf::esp_app_desc!(); #[main] fn main() -> ! { // Initialize hardware with error handling let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let peripherals = esp_hal::init(config); // Initialize components let mut led = Output::new(peripherals.GPIO8, Level::Low, OutputConfig::default()); let temp_sensor = TemperatureSensor::new(peripherals.TSENS, Config::default()).unwrap(); let mut temp_buffer = TemperatureBuffer::<BUFFER_SIZE>::new(); let mut comm = TemperatureComm::new(); let mut state = SystemState::new(); // System startup esp_println::println!("🚀 ESP32-C3 Production Temperature Monitor v1.0"); esp_println::println!("📊 Buffer: {} readings | Sample rate: {}ms", BUFFER_SIZE, SAMPLE_RATE_MS); esp_println::println!("📡 JSON output every {} readings", JSON_OUTPUT_INTERVAL); esp_println::println!("🏥 Health reports every {} readings", HEALTH_REPORT_INTERVAL); esp_println::println!("✅ System initialized successfully"); esp_println::println!(); comm.init(0); // Main production loop with error handling loop { // Read temperature with error handling let esp_temperature = temp_sensor.get_temperature(); let temp_celsius = esp_temperature.to_celsius(); let temperature = Temperature::from_celsius(temp_celsius); // Update system state state.last_temp = temp_celsius; temp_buffer.push(temperature); state.advance_time(); // LED status indication if temperature.is_overheating() { state.overheating_count += 1; // Rapid triple blink for overheating for _ in 0..3 { led.set_high(); let blink_start = Instant::now(); while blink_start.elapsed() < Duration::from_millis(100) {} led.set_low(); let blink_start = Instant::now(); while blink_start.elapsed() < Duration::from_millis(100) {} } } else if !temperature.is_normal_range() { // Double blink for abnormal range led.set_high(); let blink_start = Instant::now(); while blink_start.elapsed() < Duration::from_millis(150) {} led.set_low(); let blink_start = Instant::now(); while blink_start.elapsed() < Duration::from_millis(100) {} led.set_high(); let blink_start = Instant::now(); while blink_start.elapsed() < Duration::from_millis(150) {} led.set_low(); } else { // Normal single blink led.set_high(); let blink_start = Instant::now(); while blink_start.elapsed() < Duration::from_millis(200) {} led.set_low(); } // JSON output every N readings if state.reading_count % JSON_OUTPUT_INTERVAL == 0 { let reading_json = comm.latest_reading_json(&temp_buffer, state.system_time_ms); esp_println::println!("READING: {}", reading_json); let stats_json = comm.stats_json(&temp_buffer, state.system_time_ms); esp_println::println!("STATS: {}", stats_json); } // Health report every N readings if state.reading_count % HEALTH_REPORT_INTERVAL == 0 { esp_println::println!("HEALTH: readings={} overheating={} errors={} uptime={}ms", state.reading_count, state.overheating_count, state.sensor_error_count, state.system_time_ms ); } // Wait for next sample let wait_start = Instant::now(); while wait_start.elapsed() < Duration::from_millis(SAMPLE_RATE_MS as u64) {} } }
Production Deployment
Build and deploy the production system:
# Run tests
cargo test
# Build and deploy to ESP32-C3 (recommended)
cargo run --release
# Alternative: Build then flash separately
cargo build --release --target riscv32imc-unknown-none-elf
cargo espflash flash target/riscv32imc-unknown-none-elf/release/chapter17_integration
# Monitor production logs
cargo espflash monitor
Understanding Cargo Features
Cargo features are a powerful mechanism for conditional compilation in Rust projects. In embedded systems, they’re especially useful for managing different build configurations.
What Are Cargo Features?
Features allow you to:
- Enable/disable functionality at compile time
- Support multiple hardware platforms from one codebase
- Create development vs production builds
- Reduce binary size by excluding unused code
Our Feature Configuration
[features]
default = ["hardware"] # Default features enabled
hardware = ["esp-hal", "esp-println", "esp-bootloader-esp-idf"] # Real ESP32-C3 hardware
simulation = [] # Mock sensors for testing
verbose = [] # Detailed debug logging
telemetry = [] # Extended monitoring capabilities
Conditional Compilation
Use #[cfg(feature = "...")] to conditionally compile code:
#![allow(unused)] fn main() { // Different sensor implementations based on features #[cfg(feature = "hardware")] use esp_hal::tsens::{Config, TemperatureSensor}; #[cfg(feature = "simulation")] mod mock_sensor { pub struct MockTemperatureSensor { temperature: f32, } impl MockTemperatureSensor { pub fn new() -> Self { Self { temperature: 25.0 } } pub fn get_temperature(&mut self) -> f32 { // Simulate varying temperature self.temperature += (rand() % 5) as f32 - 2.0; self.temperature } } } // Optional verbose logging #[cfg(feature = "verbose")] macro_rules! debug_log { ($($arg:tt)*) => { esp_println::println!("DEBUG: {}", format!($($arg)*)); }; } #[cfg(not(feature = "verbose"))] macro_rules! debug_log { ($($arg:tt)*) => {}; } // Extended telemetry #[cfg(feature = "telemetry")] fn output_telemetry_data(state: &SystemState) { esp_println::println!("TELEMETRY: {{"); esp_println::println!(" \"uptime_ms\": {},", state.system_time_ms); esp_println::println!(" \"free_heap\": {},", get_free_heap()); esp_println::println!(" \"cpu_usage\": {},", get_cpu_usage()); esp_println::println!("}}"); } }
Building with Different Features
# Default build (hardware features enabled)
cargo build --release
# Build for simulation (no hardware needed)
cargo build --features simulation
# Build with verbose logging
cargo build --features "hardware,verbose"
# Build with all monitoring features
cargo build --features "hardware,verbose,telemetry"
# Build with only simulation and telemetry
cargo build --no-default-features --features "simulation,telemetry"
Why Features Matter in Embedded
- Binary Size: Exclude unused features to reduce Flash usage
- Testing: Run tests without hardware using simulation features
- Development: Enable verbose logging during development
- Production: Strip debug features for production builds
- Portability: Support multiple hardware platforms
Real-World Examples
#![allow(unused)] fn main() { // Production vs Development builds #[cfg(feature = "verbose")] const LOG_LEVEL: LogLevel = LogLevel::Debug; #[cfg(not(feature = "verbose"))] const LOG_LEVEL: LogLevel = LogLevel::Error; // Hardware-specific implementations #[cfg(feature = "hardware")] fn read_temperature() -> Result<f32, SensorError> { let sensor = TemperatureSensor::new(/* ... */)?; Ok(sensor.get_temperature().to_celsius()) } #[cfg(feature = "simulation")] fn read_temperature() -> Result<f32, SensorError> { // Return predictable test data Ok(23.5 + (system_time() % 10) as f32) } }
Exercise: Production System Integration
Integrate all previous components into a production-ready temperature monitoring system.
Requirements
- System Integration: Combine hardware, data structures, testing, and communication
- Cargo Features: Implement conditional compilation for different build configurations
- Error Recovery: Handle sensor failures and system errors gracefully
- Production Monitoring: Add health reporting and system metrics
- Build Optimization: Configure release builds for optimal performance
- Deployment Ready: Create scripts for easy flashing and monitoring
Starting Structure
Based on previous chapters, create the integrated system:
// src/bin/main.rs - Production system main file #![no_std] #![no_main] use esp_hal::clock::CpuClock; use esp_hal::gpio::{Level, Output, OutputConfig}; use esp_hal::main; // Conditional imports based on features #[cfg(feature = "hardware")] use esp_hal::tsens::{Config, TemperatureSensor}; // Import from your integrated library use chapter17_integration::{Temperature, TemperatureBuffer, TemperatureComm}; // Debug logging macro (only compiled with verbose feature) #[cfg(feature = "verbose")] macro_rules! debug_log { ($($arg:tt)*) => { esp_println::println!("DEBUG: {}", format_args!($($arg)*)); }; } #[cfg(not(feature = "verbose"))] macro_rules! debug_log { ($($arg:tt)*) => {}; } // Production configuration const BUFFER_SIZE: usize = 32; const SAMPLE_RATE_MS: u32 = 1000; const HEALTH_REPORT_INTERVAL: u32 = 20; // System state tracking struct SystemState { reading_count: u32, system_time_ms: u32, overheating_count: u32, sensor_error_count: u32, // TODO: Add more state fields } #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { // TODO: Implement production panic handler with logging loop {} } #[main] fn main() -> ! { // TODO: Initialize all components with feature-based configuration // TODO: Initialize sensor based on hardware vs simulation features // TODO: Add error handling for sensor initialization // TODO: Implement main monitoring loop with health checks // TODO: Add conditional telemetry and verbose logging } // TODO: Implement helper functions with feature gates: // - read_temperature_safe() with hardware/simulation branches // - update_led_status() with enhanced patterns // - output_health_report() for system monitoring // - handle_error_conditions() for error recovery // - output_telemetry_data() (telemetry feature only) // - debug logging (verbose feature only)
Implementation Tasks
-
Cargo Features Setup:
- Implement conditional sensor initialization (hardware vs simulation)
- Add debug logging macros with verbose feature
- Create feature-gated telemetry functions
-
System Integration:
- Initialize all hardware components with error handling
- Create SystemState struct to track system health
- Set up production configuration constants
-
Error Recovery:
- Implement safe temperature reading with fallback
- Add sensor error counting and recovery
- Create production panic handler with logging
-
Health Monitoring:
- Add system state tracking (uptime, errors, performance)
- Implement health report generation
- Create status indicators and LED patterns
-
Production Features:
- Configure optimized Cargo.toml profile
- Add JSON health reporting
- Test complete system integration with different features
Success Criteria
- System integrates all previous chapter components
- Cargo features work correctly (hardware, simulation, verbose, telemetry)
- Handles sensor failures without crashing
- Provides health monitoring and error reporting
- Optimized build configuration for production
- Complete JSON communication system working
- LED status indicates system health
- Recovery from common error conditions
- Different features produce different build outputs
Expected Health Report Output
🌡️ ESP32-C3 Complete Temperature Monitor System
=================================================
🔧 Hardware: ESP32-C3 @ max frequency
📊 Buffer capacity: 32 readings
⏱️ Sample rate: 1 Hz
🌡️ Overheating threshold: 52.0°C
📡 JSON output every 5 readings
💓 Health reports every 20 readings
🚀 System starting...
🟢📊 #001 | 24.3°C | Buffer: 1/32
🟢📊 #002 | 24.1°C | Buffer: 2/32
...
🟢📊 #020 | 24.8°C | Buffer: 20/32
💓 HEALTH REPORT
Uptime: 20s | Readings: 20
Buffer: 62% (20/32) | Memory: 128 bytes
Errors: 0 sensor, 0 overheating events
Current temp: 24.8°C
Build Optimization
Update your Cargo.toml with production settings:
[profile.release]
codegen-units = 1 # Better optimization
debug = false # Remove debug info
debug-assertions = false # Remove runtime checks
incremental = false # Full rebuild for optimization
lto = 'fat' # Link-time optimization
opt-level = 's' # Optimize for size
overflow-checks = false # Remove overflow checks
panic = 'abort' # Smaller panic handler
strip = true # Remove symbols
Deployment Script
Create deploy.sh:
#!/bin/bash
echo "🚀 Deploying Production Temperature Monitor..."
cargo build --release --features embedded
cargo espflash flash --monitor target/riscv32imc-unknown-none-elf/release/chapter17_integration
Extension Challenges
- Watchdog Timer: Add hardware watchdog for system recovery
- Flash Storage: Persist configuration across resets
- Over-the-Air Updates: Implement firmware update capability
- Network Integration: Connect to WiFi for remote monitoring
- Power Management: Add sleep modes for battery operation
Error Recovery Strategies
- Sensor Failure: Use last known good value, count errors
- Buffer Overflow: Circular buffer handles automatically
- Communication Error: Continue operation, log errors
- Memory Issues: Monitor stack usage, implement safeguards
- Timing Drift: Use hardware timers for precision
Testing Production System
# Run comprehensive tests
./test.sh
# Test different feature combinations
cargo check --features hardware
cargo check --features simulation
cargo check --features "hardware,verbose"
cargo check --features "hardware,telemetry"
cargo check --features "simulation,verbose,telemetry"
# Check build sizes with different features
cargo size --release # Default (hardware)
cargo size --release --features simulation # Simulation only
cargo size --release --features "hardware,verbose,telemetry" # Full featured
# Flash and monitor
chmod +x deploy.sh
./deploy.sh
Production System Features
✅ Error Handling: Graceful panic handling with recovery attempts ✅ Health Monitoring: System metrics and error counting ✅ Structured Logging: JSON output for monitoring dashboards ✅ Performance Optimization: Optimized builds for production deployment ✅ State Tracking: Comprehensive system state monitoring ✅ Production Configuration: Configurable intervals and thresholds
Next: In Chapter 18, we’ll explore advanced features and extensions to make the system even more capable.