/* --------------------------------------------------------------
Application: Filament Diameter Sensor
Release Type: 1st attempt
Class: Real Time Systems - Su 2025
AI Use: Commented inline
---------------------------------------------------------------*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
// #include "driver/adc.h"
#include "math.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_adc/adc_oneshot.h" // ADD
#include "hal/adc_types.h"
#include "esp_timer.h"
#include "esp_adc/adc_cali_scheme.h" // required for calibration scheme of the ADC channel
#include "esp_adc/adc_cali.h" // required for calibration of the ADC channel
#define LED_PIN GPIO_NUM_2 // changed per challenge to GPIO2
#define WARNING_LED_PIN GPIO_NUM_15 // low-lux threshold warning yellow LED connected to GPIO15
#define BUTTON_GPIO GPIO_NUM_4 // Button
#define LED_OFF 0
#define LED_ON 1
#define SHORT_DURATION 250 // tested to work as low as 150, but for 125, period indicated is 120 !
#define LONG_DELAY 1000
#define TINY_DELAY 50
#define ADC_DELAY 100
#define priorityX 1
#define priorityY 2
#define priorityZ 3
#define priorityT 4
#define LUX_THRESHOLD 500.0 // lux threshold for alert Computer screen likely off
#define LOG_BUFFER_SIZE 10 // Number of samples for averaging and Lux statistics
#define LDR_PIN1 GPIO_NUM_32 // GPIO32 connected to LDR AO PIN of sensore
#define LDR_ADC_CHANNEL1 ADC_CHANNEL_4
#define LDR_PIN2 GPIO_NUM_33 // GPIO32 connected to LDR AO PIN of sensore
#define LDR_ADC_CHANNEL2 ADC_CHANNEL_5
// BONUS Adding in some examples of using the built in ESP Logging capabilties!
const static char *TAG = "LOG-DEMO>";
// Synchronization
static SemaphoreHandle_t xLogMutex; // Hand off access to the buffer!
static SemaphoreHandle_t xButtonSem; // Hand off on button press!
// Shared buffer and index
static uint16_t sensor_log[LOG_BUFFER_SIZE];
static int log_index = 0;
int ledState = LED_OFF;
// These constants should match the photoresistor's "gamma" and "rl10" attributes
// see https://einstronic.com/product/photoresistor-light-sensor-module/
// gamma = (log (rl10/rl100))/(log(100/10)) : notice it isis defined as -(slope in log-log between rl and lux between 10 and 100 corresponding values)
const float RF = 10000; // Ohms, fixed resistor in voltage divider
const float RL10 = 50000; // Ohms at 10 lux
const float RL100 = 9980; // Ohms at 100 lux
const float GAMMA = -0.699861956f; // precomputed value, see below
const float INTERCEPT = 5.398158078f; // from regression fit: logL = GAMMA logR + INTERCEPT of the wokwi photoresist data
// calibration data for filament diameter
const float cal_dia1 = 1.5000f;
const float cal_dia2 = 2.0000f;
const int16_t raw_dia1 = 2048;
const int16_t raw_dia2 = 3800;
float slope = (cal_dia2 - cal_dia1)/(raw_dia2 - raw_dia1); // slope to convert mV to mm
;
const float default_nominal_diameter = 1.7500f;
const float max_difference_diameter = 0.200f;
const float min_diameter = 1.0000f;
const int16_t AnalogRange = (1 << 12) - 1; // = 4095
const float VS = 3.3; // ESP32 operating voltage
// Convert the analog value into lux value:
int analogValueCalibrated;
esp_timer_handle_t debounce_timer;
TaskHandle_t ADCTaskHandle = NULL;
// === ISR: Triggered on button press ===
void IRAM_ATTR button_isr_handler(void *arg) {
gpio_isr_handler_remove(BUTTON_GPIO); // Disable ISR
BaseType_t xHigherPrioTaskWoken = pdFALSE;
xSemaphoreGiveFromISR(xButtonSem, &xHigherPrioTaskWoken);
esp_timer_start_once(debounce_timer, 50000); // 50 ms debounce
portYIELD_FROM_ISR(xHigherPrioTaskWoken);
}
void debounce_timer_callback(void* arg) {
gpio_isr_handler_add(BUTTON_GPIO, button_isr_handler, NULL); // Re-enable ISR
}
// Utility function to log with timestamp
void log_task_event(const char *taskName, const char *eventType, TickType_t periodTicks) {
TickType_t now = xTaskGetTickCount();
unsigned long time_ms = now * portTICK_PERIOD_MS;
unsigned long period_ms = periodTicks * portTICK_PERIOD_MS;
printf("[%8lu ms] %s: %s | Period: %lu ms\n",
time_ms, taskName, eventType, period_ms);
}
void Beacon_xmit_task(void *pvParameters) {
TickType_t lastWakeTime = xTaskGetTickCount(); // Initial timestamp
while (1) {
ledState = !ledState;
gpio_set_level(LED_PIN, ledState);
TickType_t now = xTaskGetTickCount();
//printf("Beacon Transmit Task period: %lu ms\t", (unsigned long)((now - lastWakeTime) * portTICK_PERIOD_MS));
log_task_event("XmitTask", ledState ? "LED ON" : "LED OFF", now - lastWakeTime);
lastWakeTime = now;
vTaskDelay(pdMS_TO_TICKS(SHORT_DURATION));
}
}
void Monitor_task(void *pvParameters) {
TickType_t lastWakeTime = xTaskGetTickCount(); // Initial timestamp
while (1) {
TickType_t now = xTaskGetTickCount();
//printf("\n");
//printf("MonitorTask period: %lu ms | System time: %lu ms | LED = %s\n",
// (unsigned long)((now - lastWakeTime) * portTICK_PERIOD_MSpow(Rm , - GAMMA) * pow(10,-GAMMA) /RL10),
// (unsigned long)(now * portTICK_PERIOD_MS),
// ledState ? "ON" : "OFF");
char msg[64];
snprintf(msg, sizeof(msg), "Alive | LED = %s", ledState ? "ON" : "OFF");
log_task_event("MonitorTask", msg, now - lastWakeTime);
lastWakeTime = now;
vTaskDelay(pdMS_TO_TICKS(LONG_DELAY));
// vTaskDelay(pdMS_TO_TICKS(LONG_DELAY));
}
}
void Sensor_task(void *arg){
typedef struct {
adc_channel_t channel;
const char* name;
} sensor_config_t;
sensor_config_t sensors[2] = {
{LDR_ADC_CHANNEL1, "Sensor1"},
{LDR_ADC_CHANNEL2, "Sensor2"}
};
adc_oneshot_unit_handle_t adc_handle;
adc_oneshot_unit_init_cfg_t init_cfg = {
.unit_id = ADC_UNIT_1,
};
adc_oneshot_new_unit(&init_cfg, &adc_handle);
adc_oneshot_chan_cfg_t chan_cfg = {
.bitwidth = ADC_BITWIDTH_12,
.atten = ADC_ATTEN_DB_12,
};
adc_cali_handle_t cali_handle = NULL;
adc_cali_line_fitting_config_t cali_config = {
.unit_id = ADC_UNIT_1,
.atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_12,
};
ESP_ERROR_CHECK(adc_cali_create_scheme_line_fitting(&cali_config, &cali_handle));
float adcBuffer[2][LOG_BUFFER_SIZE] = {0};
int bufferIndex[2] = {0};
int bufferFilled[2] = {0};
TickType_t lastWakeTime = xTaskGetTickCount();
while (1) {
for (int i = 0; i < 2; ++i) {
int analogValue, analogValueCalibrated;
float Vout, Diameter;
adc_oneshot_config_channel(adc_handle, sensors[i].channel, &chan_cfg);
ESP_ERROR_CHECK(adc_oneshot_read(adc_handle, sensors[i].channel, &analogValue));
ESP_ERROR_CHECK(adc_cali_raw_to_voltage(cali_handle, analogValue, &analogValueCalibrated));
Vout = analogValueCalibrated; // in mV
Diameter = cal_dia1 + slope * (Vout - raw_dia1);
adcBuffer[i][bufferIndex[i]] = Diameter;
bufferIndex[i] = (bufferIndex[i] + 1) % LOG_BUFFER_SIZE;
if (bufferFilled[i] < LOG_BUFFER_SIZE) bufferFilled[i]++;
float sum = 0.0f, sumSq = 0.0f;
for (int j = 0; j < bufferFilled[i]; ++j) {
sum += adcBuffer[i][j];
sumSq += adcBuffer[i][j] * adcBuffer[i][j];
}
float meanDia = sum / bufferFilled[i];
float variance = (sumSq / bufferFilled[i]) - (meanDia * meanDia);
float stdDevDia = sqrtf(variance);
if (meanDia < min_diameter) {
log_task_event("ALERT", "⚠️ MEAN DIAMETER BELOW THRESHOLD!", 0);
gpio_set_level(WARNING_LED_PIN, LED_ON);
} else {
gpio_set_level(WARNING_LED_PIN, LED_OFF);
}
char msg[200];
snprintf(msg, sizeof(msg), " Analog = %d | Cal = %d mV | Diameter = %.6f | µDia = %.6f | σDia = %.6f",
analogValue, analogValueCalibrated, Diameter, meanDia, stdDevDia);
TickType_t now = xTaskGetTickCount();
uint16_t val = now % 4096;
if (xSemaphoreTake(xLogMutex, pdMS_TO_TICKS(10))) {
sensor_log[log_index] = val;
log_index = (log_index + 1) % LOG_BUFFER_SIZE;
xSemaphoreGive(xLogMutex);
}
log_task_event(sensors[i].name, msg, xTaskGetTickCount() - now);
vTaskDelay(pdMS_TO_TICKS(200));
}
vTaskDelayUntil(&lastWakeTime, pdMS_TO_TICKS(ADC_DELAY));
}
adc_oneshot_del_unit(adc_handle);
adc_cali_delete_scheme_line_fitting(cali_handle);
vTaskDelete(NULL);
}
// Task: Wait for button, "compress" and dump log
void Logger_task(void *arg) {
while (1) {
// Block until ISR signals button press
if (xSemaphoreTake(xButtonSem, portMAX_DELAY) == pdTRUE) {
ESP_LOGV(TAG,"logger entered based on button semaphore; will release on exit\n");
uint16_t copy[LOG_BUFFER_SIZE];
int count = 0;
// Lock mutex to read from buffer
if (xSemaphoreTake(xLogMutex, pdMS_TO_TICKS(100))) {
ESP_LOGV(TAG,"logger took log data semaphore for copy\n");
memcpy(copy, sensor_log, sizeof(copy));
count = log_index;
xSemaphoreGive(xLogMutex);
ESP_LOGV(TAG,"logger releasing log data semaphore\n");
}
// Simulate compression (calculate stats)
uint16_t min = 4095, max = 0;
uint32_t sum = 0;
for (int i = 0; i < LOG_BUFFER_SIZE; i++) {
// Optional: Dump all values
ESP_LOGI(TAG, "value[%2d] = %4d", i, copy[i]);
if (copy[i] < min) min = copy[i];
if (copy[i] > max) max = copy[i];
sum += copy[i];
}
uint16_t avg = sum / LOG_BUFFER_SIZE;
ESP_LOGV(TAG, "[LOG DUMP] readings %d: min=%d, max=%d, avg=%d", LOG_BUFFER_SIZE, min, max, avg);
// consider - this code doesn't really "dump" the buffer does it?
}
}
}
void TestButtonLevel(void *arg) {
while (1) {
int level = gpio_get_level(BUTTON_GPIO);
ESP_LOGV(TAG, "GPIO4 level = %d", level);
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void app_main() {
esp_log_level_set(TAG, ESP_LOG_INFO);
// Create debounce timer
const esp_timer_create_args_t debounce_timer_args = {
.callback = &debounce_timer_callback,
.name = "debounceTimer"
};
esp_timer_create(&debounce_timer_args, &debounce_timer);
// Create synchronization primitives
xLogMutex = xSemaphoreCreateMutex();
xButtonSem = xSemaphoreCreateBinary();
assert(xLogMutex && xButtonSem);
// Button setup for isr
gpio_install_isr_service(0);
ESP_LOGV(TAG,"configuring button\n");
gpio_reset_pin(BUTTON_GPIO);
gpio_set_direction(BUTTON_GPIO, GPIO_MODE_INPUT);
gpio_pullup_en(BUTTON_GPIO);
gpio_set_intr_type(BUTTON_GPIO, GPIO_INTR_NEGEDGE);
gpio_isr_handler_add(BUTTON_GPIO, button_isr_handler, NULL);
// LED setup
gpio_reset_pin(LED_PIN);
gpio_reset_pin(WARNING_LED_PIN);
gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);
gpio_set_direction(WARNING_LED_PIN, GPIO_MODE_OUTPUT);
// Initially start with all LED_OFF
gpio_set_level(LED_PIN, ledState);
gpio_set_level(WARNING_LED_PIN, ledState); // Ensure it starts in the OFF state
gpio_reset_pin(LDR_PIN1);
gpio_set_direction(LDR_PIN1, GPIO_MODE_INPUT); // configure input for analogRead
// adc1_config_width(ADC_WIDTH_BIT_12); // configure ADC 12-bit representation between 0 and 2^12-1=4095
// adc1_config_channel_atten(LDR_ADC_CHANNEL1, ADC_ATTEN_DB_11)
gpio_reset_pin(LDR_PIN2);
gpio_set_direction(LDR_PIN2, GPIO_MODE_INPUT); // configure input for analogRead
// adc1_config_width(ADC_WIDTH_BIT_12); // configure ADC 12-bit representation between 0 and 2^12-1=4095
// adc1_config_channel_atten(LDR_ADC_CHANNEL2, ADC_ATTEN_DB_11)
xTaskCreatePinnedToCore(Beacon_xmit_task, "BeaconXmit_task", 2048, NULL, priorityX, NULL,1);
xTaskCreatePinnedToCore(Monitor_task, "Monitor_task", 2048, NULL, priorityY, NULL,1);
xTaskCreatePinnedToCore(Sensor_task, "Sensor_task", 4096, NULL, priorityZ, &ADCTaskHandle,1);
xTaskCreatePinnedToCore(Logger_task, "Logger_task", 4096, NULL, priorityT, NULL, 1);
xTaskCreatePinnedToCore(TestButtonLevel, "TestButton", 2048, NULL, 5, NULL, 1);
ESP_LOGV(TAG,"System ready. Press the button to dump the log.");
}