Final version 1:

reads data every 15 minutes
saves data persistently every 24h
writes out the contents every measure.
goes in deep sleep so very low power consumption. (reason removal task system)
This commit is contained in:
2026-04-30 09:06:49 +02:00
parent 1d246d6d8a
commit dfaff67fab
353 changed files with 24553 additions and 20962 deletions

View File

@@ -1,10 +1,10 @@
#include <stdint.h>
#include <stdio.h>
#include <inttypes.h>
#include <locale.h>
#include <time.h>
#include <string.h>
#include <sys/time.h>
#include "esp_spiffs.h"
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
@@ -16,18 +16,19 @@
#include "esp_flash.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "esp_spiffs.h"
#include "esp_sleep.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
#define PRINT_DEBUG false
#define WIPE_MEMORY false
#define DHT11_GPIO 0
#define BUTTON_PIN 9 // BOOT button on ESP32-C3 SuperMini
#define BLINK_GPIO 8
#define DATA_BUFFER_SIZE 100
#define MAX_MEASUREMENTS_PER_DAY 96 // 96 * 15 minutes = 24 hours
static int measurement_count = 0;
QueueHandle_t sensor_data_queue;
SemaphoreHandle_t xMutex;
typedef struct {
char timestamp[64];
@@ -35,50 +36,59 @@ typedef struct {
float temperature;
} SensorData;
TaskHandle_t file_handler_task_handle;
// RTC buffer to store time
RTC_DATA_ATTR time_t rtc_unix_time = 0;
RTC_DATA_ATTR bool rtc_time_initialized = false;
// RTC buffer to store measurements
RTC_DATA_ATTR SensorData rtc_measurement_buffer[DATA_BUFFER_SIZE];
RTC_DATA_ATTR int rtc_buffer_index = 0;
// ############### Base Setup ###############
void set_system_time_from_compiler() {
// Parse __DATE__: "Mmm dd yyyy"
char date[] = __DATE__;
char time[] = __TIME__;
void set_system_time() {
if (!rtc_time_initialized) {
// First boot: use compile time
struct tm tm = {0};
char month[4];
int day, year, hour, min, sec;
struct tm tm = {0};
char month[4];
int day, year;
int hour, min, sec;
sscanf(__DATE__, "%3s %d %d", month, &day, &year);
sscanf(__TIME__, "%d:%d:%d", &hour, &min, &sec);
// Parse date
sscanf(date, "%3s %d %d", month, &day, &year);
// Parse time
sscanf(time, "%d:%d:%d", &hour, &min, &sec);
// Convert month abbreviation to number
const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
for (int i = 0; i < 12; i++) {
if (strcmp(month, months[i]) == 0) {
tm.tm_mon = i;
break;
const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
for (int i = 0; i < 12; i++) {
if (strcmp(month, months[i]) == 0) {
tm.tm_mon = i;
break;
}
}
}
tm.tm_mday = day;
tm.tm_year = year - 1900; // Years since 1900
tm.tm_hour = hour;
tm.tm_min = min;
tm.tm_sec = sec;
tm.tm_isdst = -1; // Let system determine daylight saving time
tm.tm_mday = day;
tm.tm_year = year - 1900;
tm.tm_hour = hour;
tm.tm_min = min;
tm.tm_sec = sec;
tm.tm_isdst = -1;
// Set system time
struct timeval tv = { .tv_sec = mktime(&tm), .tv_usec = 0 };
rtc_unix_time = mktime(&tm);
rtc_time_initialized = true;
}
// Set system time from RTC
struct timeval tv = { .tv_sec = rtc_unix_time, .tv_usec = 0 };
settimeofday(&tv, NULL);
}
void print_chip_info(){
/* Print chip information */
#if PRINT_DEBUG
void get_current_time(char* time_string, size_t size) {
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
strftime(time_string, size, "%Y-%m-%d %H:%M:%S", &timeinfo);
}
void print_chip_info() {
esp_chip_info_t chip_info;
uint32_t flash_size;
esp_chip_info(&chip_info);
@@ -97,135 +107,96 @@ void print_chip_info(){
printf("Get flash size failed");
return;
}
printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024),
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external");
printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size());
#endif
}
// ############### Measurement task ###############
// ############### Status indicator ###############
void get_current_time(char* time_string, size_t size) {
time_t now;
struct tm timeinfo;
time(&now);
localtime_r(&now, &timeinfo);
strftime(time_string, size, "%Y-%m-%d %H:%M:%S", &timeinfo);
void init_led() {
gpio_reset_pin(BLINK_GPIO);
gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(BLINK_GPIO, 1);
}
void led_flicker(int times, bool fast) {
for(u_int8_t i = 0; i < times; i++) {
gpio_set_level(BLINK_GPIO, 0);
vTaskDelay(pdMS_TO_TICKS(fast ? 250 : 500));
gpio_set_level(BLINK_GPIO, 1);
vTaskDelay(pdMS_TO_TICKS(fast ? 250 : 500));
}
}
// ############### Measurement ###############
void read_dht11(float *humidity, float *temperature) {
uint8_t data[5] = {0};
// Send start signal: pull the line low for 20ms
gpio_set_direction(DHT11_GPIO, GPIO_MODE_OUTPUT);
gpio_set_level(DHT11_GPIO, 0);
vTaskDelay(pdMS_TO_TICKS(20));
// Release the line and wait for sensor response
gpio_set_level(DHT11_GPIO, 1);
gpio_set_direction(DHT11_GPIO, GPIO_MODE_INPUT);
// Wait for the sensor to pull the line low (~20-40us)
while (gpio_get_level(DHT11_GPIO) == 1) {
vTaskDelay(pdMS_TO_TICKS(0.1));
}
while (gpio_get_level(DHT11_GPIO) == 1) vTaskDelay(pdMS_TO_TICKS(0.1));
while (gpio_get_level(DHT11_GPIO) == 0) vTaskDelay(pdMS_TO_TICKS(0.1));
// Wait for the sensor to pull the line high (~80us)
while (gpio_get_level(DHT11_GPIO) == 0) {
vTaskDelay(pdMS_TO_TICKS(0.1));
}
// Read 40 bits of data
for (int i = 0; i < 40; i++) {
// Wait for the start of the bit (low for ~50us)
while (gpio_get_level(DHT11_GPIO) == 1) {
vTaskDelay(pdMS_TO_TICKS(0.1));
}
while (gpio_get_level(DHT11_GPIO) == 0) {
vTaskDelay(pdMS_TO_TICKS(0.1));
}
while (gpio_get_level(DHT11_GPIO) == 1) vTaskDelay(pdMS_TO_TICKS(0.1));
while (gpio_get_level(DHT11_GPIO) == 0) vTaskDelay(pdMS_TO_TICKS(0.1));
// Measure the length of the high pulse
uint32_t pulse_start = esp_timer_get_time();
while (gpio_get_level(DHT11_GPIO) == 1) {
vTaskDelay(pdMS_TO_TICKS(0.1));
}
while (gpio_get_level(DHT11_GPIO) == 1) vTaskDelay(pdMS_TO_TICKS(0.1));
uint32_t pulse_end = esp_timer_get_time();
// Determine if the bit is 0 or 1
uint8_t bit = (pulse_end - pulse_start) > 50 ? 1 : 0;
data[i / 8] <<= 1;
data[i / 8] |= bit;
}
// Parse the data
uint8_t humidity_int = data[0];
uint8_t humidity_dec = data[1]; // Decimal part (0-9)
int8_t temperature_int = (data[2]); // signed value
uint8_t temperature_dec = data[3]; // Decimal part (0-9)
uint8_t humidity_dec = data[1];
int8_t temperature_int = (data[2]);
uint8_t temperature_dec = data[3];
uint8_t checksum = data[4];
#if PRINT_DEBUG
printf("\nReadout values: [hi: %d, hd: %d, ti %d, td %d, c: %d]\n", humidity_int, humidity_dec, temperature_int, temperature_dec, checksum);
#endif
// Validate checksum
uint8_t sum = humidity_int + humidity_dec + temperature_int + temperature_dec;
if (sum != checksum) {
#if PRINT_DEBUG
printf("Checksum failed! Retrying...\n");
#if PRINT_DEBUG
printf("Checksum failed! Retrying...\n");
#endif
vTaskDelay(pdMS_TO_TICKS(1000));
read_dht11(humidity, temperature);
return;
}
#if PRINT_DEBUG
printf("Checksum checks out!\n");
#endif
// Calculate humidity and temperature
*humidity = (float)humidity_int + (float)humidity_dec / 10.0f;
*temperature = (float)temperature_int + (float)temperature_dec / 10.0f;
}
void measure_task(void *pvParameters){
while (1) {
float humidity, temperature;
read_dht11(&humidity, &temperature);
char time_string[64] = {0};
get_current_time(time_string, sizeof(time_string));
void make_measurement() {
float humidity, temperature;
read_dht11(&humidity, &temperature);
#if PRINT_DEBUG
printf("Measurment: [%s, %.1f%%, %.1f°C]\n", time_string, humidity, temperature);
#endif
char time_string[64] = {0};
get_current_time(time_string, sizeof(time_string));
SensorData data;
strncpy(data.timestamp, time_string, sizeof(data.timestamp));
data.humidity = humidity;
data.temperature = temperature;
#if PRINT_DEBUG
printf("Measurement: [%s, %.1f%%, %.1f°C]\n", time_string, humidity, temperature);
#endif
// Send data to the queue
if (xQueueSend(sensor_data_queue, &data, pdMS_TO_TICKS(100)) != pdPASS) {
#if PRINT_DEBUG
printf("Failed to add measurement to queue!\n");
#endif
} else {
measurement_count++;
if (measurement_count >= MAX_MEASUREMENTS_PER_DAY) {
// Signal the file handler to write and reset
xTaskNotifyGive(file_handler_task_handle);
}
}
// Wait 15 minute before next read
vTaskDelay(pdMS_TO_TICKS(15 * 60 * 1000));
// Store in RTC buffer
if (rtc_buffer_index < DATA_BUFFER_SIZE) {
strncpy(rtc_measurement_buffer[rtc_buffer_index].timestamp, time_string, sizeof(time_string));
rtc_measurement_buffer[rtc_buffer_index].humidity = humidity;
rtc_measurement_buffer[rtc_buffer_index].temperature = temperature;
rtc_buffer_index++;
}
}
// ############### Filehandling task ###############
// ############### IO operations ###############
void init_filesystem() {
esp_vfs_spiffs_conf_t conf = {
@@ -237,13 +208,7 @@ void init_filesystem() {
esp_err_t ret = esp_vfs_spiffs_register(&conf);
if (ret != ESP_OK) {
#if PRINT_DEBUG
if (ret == ESP_FAIL) {
printf("Failed to mount or format filesystem\n");
} else if (ret == ESP_ERR_NOT_FOUND) {
printf("Failed to find SPIFFS partition\n");
} else {
printf("Failed to initialize SPIFFS (%s)\n", esp_err_to_name(ret));
}
printf("Failed to initialize SPIFFS (%s)\n", esp_err_to_name(ret));
#endif
return;
}
@@ -252,145 +217,96 @@ void init_filesystem() {
#endif
}
void file_handler_task(void *pvParameters) {
while (1) {
// Wait for notification from measurement task
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
void save_buffer_to_persistent(){
FILE *f = fopen("/spiffs/measurements.csv", "a");
if (f == NULL) {
printf("Failed to open CSV file!\n");
return;
}
SensorData data;
FILE *f = NULL;
xSemaphoreTake(xMutex, portMAX_DELAY);
f = fopen("/spiffs/measurements.csv", "a");
if (f == NULL) {
#if PRINT_DEBUG
printf("Failed to open CSV file!\n");
#endif
xSemaphoreGive(xMutex);
continue;
}
// Write header if file is empty
fseek(f, 0, SEEK_END);
if (ftell(f) == 0) {
fprintf(f, "Timestamp,Humidity,Temperature\n");
}
// Write header if file is empty
fseek(f, 0, SEEK_END);
if (ftell(f) == 0) {
fprintf(f, "Timestamp,Humidity,Temperature\n");
}
// Read all data from the queue and write to CSV
while (xQueueReceive(sensor_data_queue, &data, 0) == pdPASS) {
fprintf(f, "%s,%.1f,%.1f\n", data.timestamp, data.humidity, data.temperature);
#if PRINT_DEBUG
printf("%s,%.1f,%.1f\n", data.timestamp, data.humidity, data.temperature);
#endif
}
// Write all buffered measurements
for (int i = 0; i < rtc_buffer_index; i++) {
fprintf(f, "%s,%.1f,%.1f\n",
rtc_measurement_buffer[i].timestamp,
rtc_measurement_buffer[i].humidity,
rtc_measurement_buffer[i].temperature);
}
fclose(f);
rtc_buffer_index = 0; // Reset buffer
#if PRINT_DEBUG
printf("CSV file updated with %d measurements!\n", DATA_BUFFER_SIZE);
#endif
}
void clear_file(){
led_flicker(10, false);
FILE *f = fopen("/spiffs/measurements.csv", "w");
if (f) {
fprintf(f, "Timestamp,Humidity,Temperature\n");
fclose(f);
measurement_count = 0;
#if PRINT_DEBUG
printf("CSV file updated!\n");
#endif
xSemaphoreGive(xMutex);
}
}
// ############### Button task ###############
void button_task(void *pvParameters) {
// Configure BOOT button (GPIO9, active low with internal pullup)
gpio_set_direction(BUTTON_PIN, GPIO_MODE_INPUT);
gpio_set_pull_mode(BUTTON_PIN, GPIO_PULLUP_ONLY);
uint32_t last_press_tick = 0;
bool waiting_for_double = false;
while (1) {
if (gpio_get_level(BUTTON_PIN) == 0) { // Button pressed (active low)
vTaskDelay(pdMS_TO_TICKS(50)); // Debounce
if (gpio_get_level(BUTTON_PIN) == 0) {
uint32_t press_start = xTaskGetTickCount();
// Wait for release
while (gpio_get_level(BUTTON_PIN) == 0) {
vTaskDelay(pdMS_TO_TICKS(10));
}
vTaskDelay(pdMS_TO_TICKS(50)); // Debounce release
uint32_t press_duration = xTaskGetTickCount() - press_start;
// Only count short presses (< 500ms)
if (press_duration < pdMS_TO_TICKS(500)) {
if (waiting_for_double) {
// ===== DOUBLE PRESS: Reset CSV =====
xSemaphoreTake(xMutex, portMAX_DELAY);
FILE *f = fopen("/spiffs/measurements.csv", "w");
if (f) {
fprintf(f, "Timestamp,Humidity,Temperature\n");
fclose(f);
}
xSemaphoreGive(xMutex);
waiting_for_double = false;
} else {
// ===== FIRST PRESS: Wait for possible double press =====
waiting_for_double = true;
last_press_tick = press_start;
}
}
}
} else {
// Check if double-press window expired
if (waiting_for_double && (xTaskGetTickCount() - last_press_tick) > pdMS_TO_TICKS(500)) {
xTaskNotifyGive(file_handler_task_handle);
// ===== SINGLE PRESS: Print CSV contents (via USB CDC) =====
xSemaphoreTake(xMutex, portMAX_DELAY);
FILE *f = fopen("/spiffs/measurements.csv", "r");
if (f) {
char line[256];
while (fgets(line, sizeof(line), f)) {
printf("%s", line); // Goes to USB CDC (/dev/ttyACM0)
}
fclose(f);
}
xSemaphoreGive(xMutex);
waiting_for_double = false;
}
void readout_persistent_as_csv(){
FILE *f = fopen("/spiffs/measurements.csv", "r");
if (f) {
char line[256];
while (fgets(line, sizeof(line), f)) {
printf("%s", line);
}
vTaskDelay(pdMS_TO_TICKS(10));
fclose(f);
}
}
// ############### Shared buffer ###############
// ############### Buffer operations ###############
void init_shared_buffer() {
sensor_data_queue = xQueueCreate(DATA_BUFFER_SIZE, sizeof(SensorData));
xMutex = xSemaphoreCreateMutex();
if (sensor_data_queue == NULL || xMutex == NULL) {
#if PRINT_DEBUG
printf("Failed to create queue or mutex!\n");
#endif
void readout_buffer_as_csv(){
for (int i = 0; i < rtc_buffer_index; i++) {
printf("%s,%.1f,%.1f\n",
rtc_measurement_buffer[i].timestamp,
rtc_measurement_buffer[i].humidity,
rtc_measurement_buffer[i].temperature);
}
}
// ############### Main thread ###############
void app_main(void)
{
// Setup
setlocale(LC_ALL, "C"); // Use ASCII for text
set_system_time_from_compiler();
init_shared_buffer();
const uint16_t sleep_time_seconds = 15 * 60; // Measurement frequency
void app_main() {
setlocale(LC_ALL, "C");
set_system_time();
init_led();
init_filesystem();
// Intro
led_flicker(3, true);
#if PRINT_DEBUG
printf("DHT11 temperature and humidity sensor\n");
char start_time[64] = {0};
get_current_time(start_time, sizeof(start_time));
printf("Program started at: %s\n", start_time);
print_chip_info();
printf("\n\n=============\n\n");
#endif
// Run tasks
xTaskCreate(measure_task, "MeasureTask", 4096, NULL, 3, NULL);
xTaskCreate(file_handler_task, "FileHandlerTask", 4096, NULL, 4, &file_handler_task_handle);
xTaskCreate(button_task, "ButtonTask", 2048, NULL, 2, NULL);
}
make_measurement();
if (rtc_buffer_index >= DATA_BUFFER_SIZE) {
save_buffer_to_persistent();
}
readout_persistent_as_csv();
readout_buffer_as_csv();
#if WIPE_MEMORY
clear_file();
#endif
rtc_unix_time += sleep_time_seconds;
esp_sleep_enable_timer_wakeup(sleep_time_seconds * 1000000);
esp_deep_sleep_start();
}