# 5.8 Advanced Project: A Multi-Sensor Data Logger

<span class="ng-star-inserted">In this chapter, we will build a complete data logging application that utilizes all the core concepts we have learned: hardware interrupts for precise data acquisition, queues for safe data transfer, multiple tasks with different priorities for processing and logging, and a software timer for periodic status checks.</span>

### <span class="ng-star-inserted">Project Goal</span>

<span class="ng-star-inserted">We will create a system that performs the following actions:</span>

1. <span class="ng-star-inserted">A </span>**<span class="ng-star-inserted">hardware timer</span>**<span class="ng-star-inserted"> will generate an interrupt every 200 milliseconds.</span>
2. <span class="ng-star-inserted">The </span>**<span class="ng-star-inserted">Interrupt Service Routine (ISR)</span>**<span class="ng-star-inserted"> will simulate reading data from two sensors (e.g., temperature and humidity) and will send this data package to a queue.</span>
3. <span class="ng-star-inserted">A high-priority </span>**<span class="ng-star-inserted">"Processing Task"</span>**<span class="ng-star-inserted"> will wait for data to arrive in the queue. When it does, it will perform a simple calculation (e.g., calculate an average) and place the result into a second queue.</span>
4. <span class="ng-star-inserted">A low-priority </span>**<span class="ng-star-inserted">"Logging Task"</span>**<span class="ng-star-inserted"> will wait for results to arrive in the second queue and print them to the Serial Monitor.</span>
5. <span class="ng-star-inserted">A </span>**<span class="ng-star-inserted">software timer</span>**<span class="ng-star-inserted"> will fire every 5 seconds to print a "System OK" status message, demonstrating a non-critical, periodic background action.</span>

<span class="ng-star-inserted">This architecture is a common and robust pattern in embedded systems. It decouples the time-critical data acquisition (in the ISR) from the less critical data processing and logging (in the tasks), ensuring the system remains responsive.</span>

### <span class="ng-star-inserted">You Will Need</span>

- <span class="ng-star-inserted">An ESP32 development board.</span>
- <span class="ng-star-inserted">The Arduino IDE with the ESP32 board package installed.</span>

```c++
#include <Arduino.h>

// Define task priorities
#define PROCESSING_TASK_PRIORITY 2
#define LOGGING_TASK_PRIORITY    1

// Define handles for RTOS objects
QueueHandle_t sensorDataQueue;
QueueHandle_t resultQueue;
TimerHandle_t systemHealthTimer;
hw_timer_t *hardwareTimer = NULL;

// Data structure to hold raw sensor readings
typedef struct {
  int temperature;
  int humidity;
} SensorData;

// Data structure for the processed result
typedef struct {
  float averageValue;
} ProcessedResult;

// --- Interrupt Service Routine ---
// This function runs every time the hardware timer fires.
// It must be fast and non-blocking.
void IRAM_ATTR onTimer() {
  // Simulate reading sensor data
  SensorData data;
  data.temperature = random(20, 30); // Simulate temp between 20-29°C
  data.humidity = random(40, 60);    // Simulate humidity between 40-59%

  // Send a COPY of the data to the queue.
  // Use the ISR-safe version of the function.
  xQueueSendFromISR(sensorDataQueue, &data, NULL);
}

// --- Software Timer Callback ---
// This function runs every time the software timer expires.
void systemHealthCallback(TimerHandle_t xTimer) {
  Serial.println("[HEALTH] System OK");
}

// --- High-Priority Task: Processing ---
void processingTask(void *parameter) {
  SensorData receivedData;
  ProcessedResult result;

  while (true) {
    // Wait indefinitely until an item arrives in the sensorDataQueue
    if (xQueueReceive(sensorDataQueue, &receivedData, portMAX_DELAY) == pdPASS) {
      // We have data, now process it.
      Serial.println("[PROCESS] Data received. Calculating average...");
      
      result.averageValue = (receivedData.temperature + receivedData.humidity) / 2.0;

      // Send the result to the logging task via the resultQueue
      xQueueSend(resultQueue, &result, portMAX_DELAY);
    }
  }
}

// --- Low-Priority Task: Logging ---
void loggingTask(void *parameter) {
  ProcessedResult receivedResult;

  while (true) {
    // Wait indefinitely until a result arrives in the resultQueue
    if (xQueueReceive(resultQueue, &receivedResult, portMAX_DELAY) == pdPASS) {
      // We have a result, now log it to the console.
      Serial.print("[LOG] Processed Average: ");
      Serial.println(receivedResult.averageValue);
      Serial.println("--------------------");
    }
  }
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("--- Multi-Sensor Data Logger ---");

  // 1. Create the queues
  // Queue to hold 10 SensorData structs
  sensorDataQueue = xQueueCreate(10, sizeof(SensorData));
  // Queue to hold 5 ProcessedResult structs
  resultQueue = xQueueCreate(5, sizeof(ProcessedResult));

  // 2. Create the software timer for system health checks
  systemHealthTimer = xTimerCreate(
      "HealthTimer",          // Name
      pdMS_TO_TICKS(5000),    // 5000ms period
      pdTRUE,                 // Auto-reload
      (void *) 0,             // Timer ID
      systemHealthCallback    // Callback function
  );

  // 3. Create the tasks
  xTaskCreate(
      processingTask,         // Function to implement the task
      "Processing Task",      // Name of the task
      2048,                   // Stack size in words
      NULL,                   // Task input parameter
      PROCESSING_TASK_PRIORITY, // Priority of the task
      NULL                    // Task handle
  );

  xTaskCreate(
      loggingTask,
      "Logging Task",
      2048,
      NULL,
      LOGGING_TASK_PRIORITY,
      NULL
  );

  // 4. Configure the hardware timer
  // Use a prescaler of 80 to get a 1MHz clock (80MHz / 80)
  hardwareTimer = timerBegin(0, 80, true);
  timerAttachInterrupt(hardwareTimer, &onTimer, true);
  // Set alarm for 200,000 counts (200ms at 1MHz)
  timerAlarmWrite(hardwareTimer, 200000, true); 
  timerAlarmEnable(hardwareTimer);

  // 5. Start the software timer
  if (systemHealthTimer != NULL) {
    xTimerStart(systemHealthTimer, 0);
  }
  
  Serial.println("System initialized. Starting data acquisition...");
}

void loop() {
  // The main loop is empty. All work is done by RTOS tasks and timers.
  vTaskDelete(NULL); // Delete the loop task to save resources
}
```

### <span class="ng-star-inserted">Code Walkthrough</span>

1. **<span class="ng-star-inserted">Data Structures:</span>**<span class="ng-star-inserted"> We define two </span>`<span class="inline-code ng-star-inserted">structs</span>`<span class="ng-star-inserted">, </span>`<span class="inline-code ng-star-inserted">SensorData</span>`<span class="ng-star-inserted"> and </span>`<span class="inline-code ng-star-inserted">ProcessedResult</span>`<span class="ng-star-inserted">, to create organized data packages that can be sent to queues. This is much cleaner than passing raw variables.</span>
2. **<span class="ng-star-inserted">Hardware Interrupt (</span>`<span class="inline-code ng-star-inserted">onTimer</span>`<span class="ng-star-inserted">)</span>**<span class="ng-star-inserted">: This ISR is the "producer." It runs at a precise interval, generates data, and immediately sends it to the </span>`<span class="inline-code ng-star-inserted">sensorDataQueue</span>`<span class="ng-star-inserted">. It does no processing and exits quickly, as a good ISR should.</span>
3. **<span class="ng-star-inserted">Processing Task (</span>`<span class="inline-code ng-star-inserted">processingTask</span>`<span class="ng-star-inserted">)</span>**<span class="ng-star-inserted">: This task is a "consumer-producer." It has a higher priority because we want to process data as soon as it's available. It waits on </span>`<span class="inline-code ng-star-inserted">sensorDataQueue</span>`<span class="ng-star-inserted">. When data arrives, it performs a quick calculation and sends the result to </span>`<span class="inline-code ng-star-inserted">resultQueue</span>`<span class="ng-star-inserted">.</span>
4. **<span class="ng-star-inserted">Logging Task (</span>`<span class="inline-code ng-star-inserted">loggingTask</span>`<span class="ng-star-inserted">)</span>**<span class="ng-star-inserted">: This is the final "consumer." It has a lower priority because logging is generally not a time-critical operation. It waits for fully processed data on </span>`<span class="inline-code ng-star-inserted">resultQueue</span>`<span class="ng-star-inserted"> and prints it. Because of its lower priority, it will only run when the </span>`<span class="inline-code ng-star-inserted">processingTask</span>`<span class="ng-star-inserted"> is blocked (waiting for data).</span>
5. **<span class="ng-star-inserted">Software Timer (</span>`<span class="inline-code ng-star-inserted">systemHealthCallback</span>`<span class="ng-star-inserted">)</span>**<span class="ng-star-inserted">: This runs completely independently of the main data flow. Every 5 seconds, it prints a status message, demonstrating how you can easily add periodic, non-critical background functions to your application without interfering with the main logic.</span>
6. **`<span class="inline-code ng-star-inserted">setup()</span>`<span class="ng-star-inserted"> Function</span>**<span class="ng-star-inserted">: This is where we initialize all our RTOS objects. We create the queues, create the tasks, configure the hardware timer, and start the software timer.</span>
7. **`<span class="inline-code ng-star-inserted">loop()</span>`<span class="ng-star-inserted"> Function</span>**<span class="ng-star-inserted">: In a complex RTOS application, the main </span>`<span class="inline-code ng-star-inserted">loop()</span>`<span class="ng-star-inserted"> function is often no longer needed. All continuous work is handled by tasks. We can delete the loop task with </span>`<span class="inline-code ng-star-inserted">vTaskDelete(NULL)</span>`<span class="ng-star-inserted"> to free up its stack memory.</span>

### <span class="ng-star-inserted">How to Test It</span>

1. <span class="ng-star-inserted">Upload the code to your ESP32.</span>
2. <span class="ng-star-inserted">Open the Arduino Serial Monitor at 115200 baud.</span>
3. <span class="ng-star-inserted">You should see the following pattern:</span>
    
    
    - <span class="ng-star-inserted">Every 200 milliseconds, the "\[PROCESS\]" message will appear, indicating the high-priority task has received data from the ISR.</span>
    - <span class="ng-star-inserted">Immediately after, the "\[LOG\]" message will print the calculated average, followed by a separator.</span>
    - <span class="ng-star-inserted">Every 5 seconds, a "\[HEALTH\] System OK" message will appear, independently of the other messages.</span>