5.8 Advanced Project: A Multi-Sensor Data Logger 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. Project Goal We will create a system that performs the following actions: A  hardware timer  will generate an interrupt every 200 milliseconds. The  Interrupt Service Routine (ISR)  will simulate reading data from two sensors (e.g., temperature and humidity) and will send this data package to a queue. A high-priority  "Processing Task"  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. A low-priority  "Logging Task"  will wait for results to arrive in the second queue and print them to the Serial Monitor. A  software timer  will fire every 5 seconds to print a "System OK" status message, demonstrating a non-critical, periodic background action. 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. You Will Need An ESP32 development board. The Arduino IDE with the ESP32 board package installed. #include // 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 } Code Walkthrough Data Structures:  We define two  structs ,  SensorData  and  ProcessedResult , to create organized data packages that can be sent to queues. This is much cleaner than passing raw variables. Hardware Interrupt ( onTimer ) : This ISR is the "producer." It runs at a precise interval, generates data, and immediately sends it to the  sensorDataQueue . It does no processing and exits quickly, as a good ISR should. Processing Task ( processingTask ) : 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  sensorDataQueue . When data arrives, it performs a quick calculation and sends the result to  resultQueue . Logging Task ( loggingTask ) : 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  resultQueue  and prints it. Because of its lower priority, it will only run when the  processingTask  is blocked (waiting for data). Software Timer ( systemHealthCallback ) : 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. setup()  Function : 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. loop()  Function : In a complex RTOS application, the main  loop()  function is often no longer needed. All continuous work is handled by tasks. We can delete the loop task with  vTaskDelete(NULL)  to free up its stack memory. How to Test It Upload the code to your ESP32. Open the Arduino Serial Monitor at 115200 baud. You should see the following pattern: Every 200 milliseconds, the "[PROCESS]" message will appear, indicating the high-priority task has received data from the ISR. Immediately after, the "[LOG]" message will print the calculated average, followed by a separator. Every 5 seconds, a "[HEALTH] System OK" message will appear, independently of the other messages.