PID control with slow sample rate (1 min)

Hi there,

I'm making a Dew Controller. I'm kinda new to this, and ChatGPT has helped me do things i never could have otherwise, so I'm in a weird position of trying to implement code that compiles, but I don't fully understand.

Anyway, the idea is that the Dew Controller compares Ambient Temperature and Humidity to the temperature of my telescope mirror. It heats it up via PWM to keep it just above the calculated dewpoint.

The problem is the PID controls just don't seem to have any real effect. The integral saturates at -75 every time within a few iterations, and the mirror is kept a constant 1-1.5 degrees above the target temperature (like an offset). As a system, it's slow in that sampling is once per minute, and doubly slow in that there is a lag when heat is applied, to when the sensor detects it. As i said, I'm pretty new, so maybe me and Chat GPT just arent aware of some big issue.

The relevant variables are below, followed by the code.

Thanks for any help, I'm really pulling my hair out here!

Markus

Kp = 6;
Ki = 0.0001;
Kd = 0.8;
maxRampRate = 1;
decayFactor = 0.85;
Deadband = -+0.5 deg.

Code Below. And I'll include some test data below that

//Libraries
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <algorithm>  // Include the algorithm library for max_element and min_element
#include <cfloat>  // For FLT_MAX and FLT_MIN




#define SCREEN_WIDTH 128     // OLED display width, in pixels
#define SCREEN_HEIGHT 64     // OLED display height, in pixels
#define OLED_RESET -1        // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C  ///< See datasheet for Address; 0x3D for 128x64,0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Define to which pin of the Arduino the 1-Wire bus is connected:
#define ONE_WIRE_BUS 8
#define DHTPIN 2       // what pin we're connected to
#define DHTTYPE DHT22  // DHT 22  (AM2302)

// Create a new instance of the oneWire class to communicate with any OneWire device:
OneWire oneWire(ONE_WIRE_BUS);
// Pass the oneWire reference to DallasTemperature library:
DallasTemperature sensors(&oneWire);

//Constants

DHT dht(DHTPIN, DHTTYPE);  //// Initialize DHT sensor for normal 16mhz Arduino

// const int mtemp_pin = 2;
// const int atemp_pin = 8;
const int dewPWM = 5; /*does the program actually output PWM to the pin, or just calculate it while being useless?*/
const int mtemp_pin = 8;
const int atemp_pin = 2;
//onewirebus is pin 8 as defined elsewhere

//Variables
float hum = 0;                    //Stores humidity value
float atemp = 0;                  //Stores temperature value
float dewpoint = 0;               //stores dew point value
float mtemp = 0;                  //current mirror temp
int power = 0;                    //power on scale of 0-255
int level = 2;                    //How many degrees above dew point the target is going to be
int brightness = 50;  //Display backlight Brightness - no effect?
float powerCent = 0;    //Power in percent
int delayAmount = 5000;
const int chipSelect = 10;             //for OLED
int fileIndex = 0;                     //counter for generating new SD files
float target = 0; 
const int marginLeft = 16;                             // 8-pixel margin on the left Space for Graph = 128-16 = 112
const int marginBottom = 3;                            // 8-pixel margin at the bottom Space left for graph = 64-0 = 64
const int graphWidth = SCREEN_WIDTH - marginLeft;      // Width of the graph area
const int graphHeight = SCREEN_HEIGHT - marginBottom;  // Height of the graph area
const int xAxisY = 61;      // X-axis line moved to pixel row 61
const int numPoints = graphWidth;                      // Number of points in the graph
const int numDataPoints = graphWidth;  // Number of data points to plot
float values[numDataPoints];  // Array to hold data points for plotting
int totalLines = 0;
float minValues[5]; // For ATemp, MTemp, Humidity, Dewpoint, Power
float maxValues[5]; // For ATemp, MTemp, Humidity, Dewpoint, Power
float minValue = 0.0;  // Global variable for the current minimum value
float maxValue = 0.0;  // Global variable for the current maximum value

// PID control constants and variables
float Kp = 6;  // (was 5) responds to the error difference. Higher = more reactive
float Ki = 0.0001;  // (was 0.0002)Integral constant, eliminate persistent offset. Higher = more reactive. Smaller values minimise overshooting
float Kd = 0.8;  // (was 0.8) Predicts rate of change. Lower values mean slower changes
float integral = 0.0;
float previousError = 0.0;
unsigned long lastUpdateTime = 0;  // For time delta calculations
float maxRampRate = 1;  // (was 1.5) Limits the rate at which power (or the control variable) can change.
float error = 0;
float decayFactor = 0.85;  // (was 0.85) Decay the integral by each iteration. Prevent windup and smooth control within a deadband.


const char Filename[]= "DEWLOG00.txt";  //The base name of the file before enumeration
String currentFileName = "DEWLOG00.txt";
float minATemp, maxATemp, minMTemp, maxMTemp, minHum, maxHum, minDewpoint, maxDewpoint, minPowerCent, maxPowerCent;


File myFile;//



// Function prototypes
void displayData();
void plotGraph();
void displayVariable(float value, int decimalPlaces);
void displayMinMax();
void calculateAllMinMaxFromSD();
float calculateDewPoint();
void loadGraphDataFromSD(int commaIndex);
// sD TROUBLESHOTING Function prototypes
void testSPI();
void testSDCard();
void listFiles();
void printDirectory(File dir, int numTabs);


void setup()  //****************************************************************************************
{
  Serial.begin(9600);

    // Rest of your setup code...
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;)
      ;  // Don't proceed, loop forever
  }

  // Show initial display buffer contents on the screen --
  // the library initializes this with an Adafruit splash screen.
  
display.dim(true);//test dimming display
  display.display();
  delay(1000);  // Pause for 2 seconds
                // Clear the buffer
                /* ****************************Splash Screen****************/
  display.clearDisplay();
  
  display.setTextSize(3);               // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);  // Draw white text
  display.setCursor(0, 0);              // Start at top-left corner
  display.println("Markus");
  display.println("Stone");
  display.setTextSize(1);
  display.println(" ");
  display.println("Dew Controller 2.5");
  display.display();
  delay(500);


lastUpdateTime = millis();

//Initialise Temp Sensors
  dht.begin();
  sensors.begin();
  // Send the command for all devices on the bus to perform a temperature conversion:

sensors.requestTemperatures();  // Request temperature readings
Serial.print("Number of devices found on OneWire bus: ");
Serial.println(sensors.getDeviceCount());

DeviceAddress tempSensorAddress;
if (sensors.getAddress(tempSensorAddress, 0)) {
    Serial.print("Sensor 0 address: ");
    for (int i = 0; i < 8; i++) {
        if (tempSensorAddress[i] < 16) Serial.print("0");  // Leading zero padding
        Serial.print(tempSensorAddress[i], HEX);
    }
    Serial.println();
} else {
    Serial.println("No sensor found at index 0.");
}


  pinMode(mtemp_pin, INPUT);
  pinMode(atemp_pin, INPUT);
  pinMode(dewPWM, OUTPUT);

//Initialise SD Card
// Initialize SD Card
Serial.print("Initializing SD card...");
if (!SD.begin(chipSelect)) {

  while (!Serial) ; // Wait for Serial Monitor to connect (for Leonardo/Micro)
  
  // Serial.println("Starting SD card debug...");

  // // Test if the SD card module responds to SPI signals
  // testSPI();

  // // Test SD card initialization
  // testSDCard();


    Serial.println("Card failed, or not present");

    // Display error message on OLED
    display.clearDisplay();
    display.setTextSize(2);               // Large font size
    display.setTextColor(SSD1306_WHITE);  // White text
    display.setCursor(0, 10);             // Position on screen
    display.println("SD Error!");
    display.setTextSize(1);               // Smaller text for details
    display.setCursor(0, 35);
    display.println("Insert SD card");
    display.println("and reboot");
    display.display();

    // Halt further execution
    while (true) {
        delay(1000);  // Infinite loop with delay
    }
}
Serial.println("Card initialized.");


myFile = SD.open("DEWLOG00.txt", FILE_WRITE);
myFile.close();

loadPIDStateFromFile(integral, previousError);

}

void loop()  //**************************************************************************************** Void Loop
{
// serial commands monitor
  if (Serial.available()) {
    String command = Serial.readStringUntil('\n');
    command.trim(); // Remove any leading/trailing whitespace

    if (command.equalsIgnoreCase("C")) {
      clearFile("DEWLOG00.txt");
    } else if (command.equalsIgnoreCase("R")) {
      readFile("DEWLOG00.txt");
    } 
  else if (command.equalsIgnoreCase("I")) {
      clearFile("DEWLOG00.txt");
      integral = 0.0;  // Reset the integral term
      Serial.println("File Cleared. Integral reset to 0.");
      return;  // Skip the rest of the loop for this iteration
  }
    else {
      Serial.println("Unknown command.");
    }
  }

  atemp = dht.readTemperature();       //ambient Temperature
    hum = dht.readHumidity();
  dewpoint = (calculateDewPoint());
target=level+dewpoint;


// Update Mirror Temperature
sensors.requestTemperatures(); // Request new temperature readings
float rawMTemp = sensors.getTempCByIndex(0);
if (rawMTemp == DEVICE_DISCONNECTED_C || rawMTemp < -50 || rawMTemp > 150) {
    Serial.println("Error: Invalid Mirror Temperature Reading!");
    return; // Skip this iteration if MTemp is invalid
}
mtemp = rawMTemp - 10; // Apply calibration offset

// Calculate error and target
error = target - mtemp;
unsigned long currentTime = millis();
float deltaTime = (currentTime - lastUpdateTime) / 1000.0; // Time in seconds
lastUpdateTime = currentTime;

// Check if error is within the deadband
if (abs(error) <= 0.5) {
  integral *= decayFactor; // Decay integral within deadband
  power = constrain(power - 1, 0, 255); // Gradual ramp-down
} else {

// PID Calculations
if (abs(error) > 0.5) {  // Deadband for small errors
  if (power < 255 && power > 0) {  // Prevent windup when output is saturated
      integral += error * deltaTime;
  } else {
      integral *= decayFactor;  // Decay integral when output is saturated
  }
  integral = constrain(integral, -75, 75);  // Constrain integral within limits
}

// Derivative calculation
float derivative = (deltaTime > 0) ? (error - previousError) / deltaTime : 0.0;
previousError = error;

// PID output calculation
float pidOutput = Kp * error + Ki * integral + Kd * derivative;

// Apply sigmoid for smoother control
float sigmoidPower = 255.0 / (1.0 + exp(-pidOutput * 0.1));
power += constrain(sigmoidPower - power, -maxRampRate * deltaTime, maxRampRate * deltaTime);
power = constrain(power, 0, 255);  // Constrain power within bounds

}

// Apply calculated power to the heating element
analogWrite(dewPWM, power);
powerCent = map(power, 0, 255, 0, 100); // Map power for display


values[fileIndex % numDataPoints] = atemp;  // Replace 'atemp' with the desired variable
fileIndex++;

  //    **************************determine appropriate power output

  

  //*****************************Max Min Test
  if (mtemp > maxMTemp) {
    maxMTemp = mtemp;
  } else if (mtemp < minMTemp) {
    minMTemp = mtemp;
  }
  if (atemp > maxATemp) {
    maxATemp = atemp;
  } else if (atemp < minATemp) {
    minATemp = atemp;
  }
  if (hum > maxHum) {
    maxHum = hum;
  } else if (hum < minHum) {
    minHum = hum;
  }
  if (powerCent > maxPowerCent) {
    maxPowerCent = powerCent;
  } else if (powerCent < minPowerCent) {
    minPowerCent = powerCent;
  }
  if (dewpoint > maxDewpoint) {
    maxDewpoint = dewpoint;
  } else if (dewpoint < minDewpoint) {
    minDewpoint = dewpoint;
  }
  //make a string for assembling the data to log:

String dataString = String(atemp);
dataString += ", ";
dataString += mtemp;
dataString += ", ";
dataString += hum;
dataString += ", ";
dataString += dewpoint;
dataString += ", ";
dataString += powerCent;
dataString += ", ";
dataString += power;
dataString += ", ";
dataString += error;
dataString += ", ";
dataString += integral;
dataString += ", ";
dataString += previousError;


Serial.println(dataString);


File myFile = SD.open(currentFileName, FILE_WRITE);
if (myFile) {
    myFile.println(dataString);
myFile.flush();
myFile.close();
  // 3. Calculate min/max values once
    calculateAllMinMaxFromSD();
  //  Serial.println("File Closed");
  //  Serial.println(dataString);
  //  Serial.println("Data written to file and serial monitor.");
} else {
    Serial.println("Error opening file for writing.");
}


  //*******************begin OLED display

displayMinMax();

//Serial.println("MinMax Display complete, now for plotGraphs");

//-----------------------Display ATemp Graph

  display.clearDisplay();

plotGraph(0);

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(95,0);             // Start at top-left corner
  display.println("ATemp");
  display.display();
  delay(delayAmount);



//----------------------Display Mirror Graph
  display.clearDisplay();
plotGraph(1);

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(95,0);             // Start at top-left corner
  display.println("MTemp");
  display.display();
  delay(delayAmount);


  //-----------------------Display Hum Graph
    display.clearDisplay();
plotGraph(2);

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(100,0);             // Start at top-left corner
  display.println("Hum");
  display.display();
  delay(delayAmount);

  //Graph Heading

  //----------------------Display Dew Point Graph
    display.clearDisplay();
plotGraph(3);

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(100,0);             // Start at top-left corner
  display.println("Dew");
  display.display();
  delay(delayAmount);


  //----------------------Display PowerCent Graph
  display.clearDisplay();
plotGraph(4);

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(100,0);             // Start at top-left corner
  display.println("Pwr%");
  display.display();
  delay(delayAmount);



  //----------------------Display Error Graph
  display.clearDisplay();
plotGraph(6);

  display.setTextSize(1);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(100,0);             // Start at top-left corner
  display.println("Err");
  display.display();
  delay(delayAmount);

    Serial.print("Total lines in file: ");
Serial.println(totalLines);

}



/* **************************************************Loop End ********************************************/

/* **************************************************Functions Defined********************************************/

//************************************graph function

// Update displayMinMax to use values from SD card
void displayMinMax() {


    // Display min/max for ATemp
display.clearDisplay();
    display.setTextSize(2);             // Normal 1:1 pixel scale
    display.setTextColor(SSD1306_WHITE);        // Draw white text
    display.setCursor(0,0);             // Start at top-left corner
    display.println("Ambient");
    display.println("");
    display.setTextSize(3);
char buffer[7]; // Create a buffer to hold the formatted string
    sprintf(buffer, "%.1f", atemp); // Format error with 1 decimal place and append "°C"
    display.print(buffer); // Send the formatted string to the display
    display.cp437(true);
    display.write(248);
    display.println("C");
    display.setTextSize(1);
    display.print("Lo = "); 
    display.print(minValues[0]); // Cached min for ATemp
    display.print(" Hi = "); 
    display.println(maxValues[0]); // Cached max for ATemp
    display.display();
    delay(delayAmount);


  // Display min/max for MTemp
display.clearDisplay();
  display.setTextSize(2);             // Normal 1:1 pixel scale
  display.setTextColor(SSD1306_WHITE);        // Draw white text
  display.setCursor(0,0);             // Start at top-left corner
    if (mtemp==-127){
      display.println("  Mirror");
  display.setTextSize(1); 
    display.println("");
        display.setTextSize(2); 
    display.println("** not **");
    display.println("attached!!");
  }
  else
  {
    display.println("Mirror");
  display.println("");
  display.setTextSize(1); 
    display.setTextSize(3); 
    sprintf(buffer, "%.1f", mtemp); // Format error with 1 decimal place and append "°C"
    display.print(buffer); // Send the formatted string to the display
  display.cp437(true);
  display.write(248);
  display.println("C");
  display.setTextSize(1); //minmax display
  display.setTextSize(1); 
    display.print("Lo = ");
    display.print(minValues[1]); // Cached min for MTemp
    display.print(" Hi = ");
    display.println(maxValues[1]); // Cached max for MTemp
    }
    display.display();
    delay(delayAmount);



    // Display min/max for Humidity
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0,0);
    display.println("Humidity");
        display.println("");
    display.setTextSize(3);
    displayVariable(hum, 1);
    display.println("%");
    display.setTextSize(1);
    display.print("Lo = ");
    display.print(minValues[2]); // Cached min for Hum
    display.print(" Hi = ");
    display.println(maxValues[2]); // Cached max for Hum
    display.display();
    delay(delayAmount);



    // Display min/max for Dew Point
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0,0);
    display.println("Dew Point");
    display.println("");
    display.setTextSize(3);
    sprintf(buffer, "%.1f", dewpoint); // Format error with 1 decimal place and append "°C"
    display.print(buffer); // Send the formatted string to the display
    display.cp437(true);
    display.write(248);
    display.println("C");
    display.setTextSize(1);
    display.print("Lo = ");
    display.print(minValues[3]); // Cached min for Dew
    display.print(" Hi = ");
    display.println(maxValues[3]); // Cached max for Dew    
    display.display();
    delay(delayAmount);

    // Display min/max for PowerCent
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0,0);
    display.println("Power");
    display.println("");
    display.setTextSize(3);
    displayVariable(powerCent, 1);
    display.println("%");
    display.setTextSize(1);
    display.print("Lo = ");
    display.print(minValues[4]); // Cached min for Pwr
    display.print(" Hi = ");
    display.println(maxValues[4]); // Cached max for Pwr
    display.display();
    delay(delayAmount);


        // Display min/max for Error
    display.clearDisplay();
    display.setTextSize(2);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0,0);
       display.println("Error");
    display.println("");
    display.setTextSize(3);
    sprintf(buffer, "%.1f", error); // Format error with 1 decimal place and append "°C"
    display.print(buffer); // Send the formatted string to the display
display.cp437(true);
    display.write(248);
    display.println("C");
    display.setTextSize(1);
    display.print("Lo = ");
    display.print(minValues[4]); // Cached min for Pwr
    display.print(" Hi = ");
    display.println(maxValues[4]); // Cached max for Pwr
    display.display();
    delay(delayAmount);
}

void calculateAllMinMaxFromSD() {
    File dataFile = SD.open("DEWLOG00.txt", FILE_READ);
    if (!dataFile) {
        Serial.println("Error opening data file for Min/Max calculation.");
        return;
    }

    for (int i = 0; i < 5; i++) {
        minValues[i] = FLT_MAX;
        maxValues[i] = FLT_MIN;
    }

    while (dataFile.available()) {
        String line = dataFile.readStringUntil('\n');
        int commaIndex = 0;

        for (int i = 0; i < 5; i++) {
            int startIndex = commaIndex;
            commaIndex = line.indexOf(',', commaIndex);
            if (commaIndex == -1) commaIndex = line.length();
            String valueString = line.substring(startIndex, commaIndex);
            float value = valueString.toFloat();

            if (!isnan(value)) {
                if (value < minValues[i]) minValues[i] = value;
                if (value > maxValues[i]) maxValues[i] = value;
            }
            commaIndex++;
        }
    }

    dataFile.close();
}


void plotGraph(int commaIndex) {
  minValue = minValues[commaIndex];
  maxValue = maxValues[commaIndex];

  loadGraphDataFromSD(commaIndex);  // Load data points from the SD card

  // Ensure the Y-axis range is at least 1 degree
  if (maxValue - minValue < 1.0) {
      float midPoint = (maxValue + minValue) / 2.0;
      minValue = midPoint - 0.5;
      maxValue = midPoint + 0.5;
  }

  display.clearDisplay();

  int labelWidth = 12;  // Y-axis labels width
  int adjustedGraphWidth = SCREEN_WIDTH - labelWidth;

  // Draw Y-axis
  display.drawLine(labelWidth, 0, labelWidth, graphHeight, SSD1306_WHITE);

// Draw Y-axis labels (integer, max of 5)
  int numLabels = 5;
  int labelStep = ceil((maxValue - minValue) / (numLabels - 1));  // Step between labels

  // Ensure labels are integers
  int roundedMinValue = floor(minValue);
  for (int i = 0; i < numLabels; i++) {
      int labelValue = roundedMinValue + i * labelStep;  // Calculate label value
      int y = map(labelValue, minValue, maxValue, graphHeight - 1, 0);  // Map label to Y coordinate

      // Draw label text
      display.setTextSize(1);
      display.setCursor(0, y - 4);  // Adjust vertical position for centering text
      display.print(labelValue);  // Print integer value
  }

  int prevY = -1;  // Previous Y-coordinate for connecting lines

  for (int i = 0; i < fileIndex; i++) {
      // Map the X coordinate directly to pixels
      int x = labelWidth + i;  // Each point gets one pixel horizontally
      if (x >= SCREEN_WIDTH) break;  // Prevent drawing beyond the screen width

      // Map the Y coordinate based on the value range
      int y = map(values[i] * 100, minValue * 100, maxValue * 100, graphHeight - 1, 0);

      // Draw a line to the previous point if it exists
      if (prevY != -1) {
          display.drawLine(x - 1, prevY, x, y, SSD1306_WHITE);
      }

      prevY = y;  // Update the previous Y-coordinate
  }

  // Draw X-axis
  display.drawLine(labelWidth, graphHeight - 1, SCREEN_WIDTH - 1, graphHeight - 1, SSD1306_WHITE);

drawTicks(fileIndex, delayAmount, SCREEN_WIDTH);
  // Refresh display
  display.display();
}





void drawTicks(int totalLines, int samplingDelay, int screenWidth) {
  int labelWidth = 12;  // Y-axis labels width
  int graphWidth = screenWidth - labelWidth;  // Adjust for graph width

  if (totalLines < 1 || graphWidth <= 0) return; // Avoid divide-by-zero or invalid states

  // Total time span in seconds
  float totalTimeSeconds = (samplingDelay / 1000.0) * totalLines;

  // Total time span in hours
  float totalTimeHours = totalTimeSeconds / 3600.0;

  // Calculate time per pixel in hours
  float timePerPixel = totalTimeHours / graphWidth;

  // Determine pixels per tick for 1-hour intervals
  int pixelsPerHourTick = max(1, (int)(1.0 / timePerPixel));  // Ensure at least 1 pixel per tick

  // Draw ticks across the X-axis
  for (int x = labelWidth; x < screenWidth; x += pixelsPerHourTick) {
      int tickYStart = graphHeight - 1;  // Use the global graphHeight
      int tickLength = 5;                // Length of tick marks

      // Draw the tick
      display.drawPixel(x, tickYStart, SSD1306_WHITE);       // Bottom pixel of the tick
      display.drawPixel(x, tickYStart - tickLength, SSD1306_WHITE);  // Extend the tick upwards
  }
}






void displayVariable(float value, int decimalPlaces) {
  int integerPart = (int)value;
  int fractionalPart = (int)((value - integerPart) * pow(10, decimalPlaces));

  display.print(integerPart);
  display.print(".");
  // Pad fractional part with leading zeros if necessary
  if (fractionalPart < 10 && decimalPlaces > 1) {
    display.print("0");
  }
  display.print(fractionalPart);
}

float calculateDewPoint() {
    const float a = 17.27;
    const float b = 237.7;
    float alpha = (a * atemp) / (b + atemp) + log(hum / 100.0);
    return (b * alpha) / (a - alpha);
}

void loadGraphDataFromSD(int commaIndex) {
    File dataFile = SD.open("DEWLOG00.txt", FILE_READ);
    if (!dataFile) {
        Serial.println("Error opening data file for graph.");
        return;
    }

    int totalValidLines = 0;
    int index = 0;

    // First pass: Count total lines in the file
    while (dataFile.available()) {
        String line = dataFile.readStringUntil('\n');
        if (line.length() > 0) {
            totalValidLines++;
        }
    }

    // Calculate the skip factor for subsampling
    int skipFactor = 1;
    if (totalValidLines > numDataPoints) {
        skipFactor = totalValidLines / numDataPoints;
    }

    // Reset file for second pass to read and subsample
    dataFile.seek(0);

    int currentLine = 0;
    totalLines = totalValidLines;  // Update global totalLines variable

    float subMin = FLT_MAX;  // Minimum value in subsampled data
    float subMax = FLT_MIN;  // Maximum value in subsampled data

    while (dataFile.available() && index < numDataPoints) {
        String line = dataFile.readStringUntil('\n');
        currentLine++;

        // Skip lines that are not part of the subsampling
        if (currentLine % skipFactor != 0 && totalValidLines > numDataPoints) {
            continue;
        }

        // Parse and validate the data
        int startIndex = 0;
        for (int i = 0; i < commaIndex; i++) {
            startIndex = line.indexOf(',', startIndex) + 1;
            if (startIndex == 0) continue;
        }
        int endIndex = line.indexOf(',', startIndex);
        if (endIndex == -1) endIndex = line.length();

        String valueString = line.substring(startIndex, endIndex);
        float value = valueString.toFloat();

        if (isnan(value)) continue;

        // Update subsampled min/max
        subMin = min(subMin, value);
        subMax = max(subMax, value);

        // Store the valid value
        values[index] = value;
        index++;
    }

    // Update fileIndex and subsampled min/max
    fileIndex = index;
    minValue = subMin;
    maxValue = subMax;

    dataFile.close();
} 


void testSPI() {
  Serial.println("\nTesting SPI connections...");

  // Set SPI pin modes (adjust for your Arduino model if needed)
  pinMode(MISO, INPUT);
  pinMode(MOSI, OUTPUT);
  pinMode(SCK, OUTPUT);
  pinMode(chipSelect, OUTPUT);

  // Perform a simple SPI transfer test
  digitalWrite(chipSelect, LOW);
  SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
  byte response = SPI.transfer(0xFF); // Send dummy byte
  SPI.endTransaction();
  digitalWrite(chipSelect, HIGH);

  if (response != 0xFF) {
    Serial.println("SPI test failed! Check MISO, MOSI, SCK, or chipSelect.");
  } else {
    Serial.println("SPI test passed.");
  }
}

void testSDCard() {
  Serial.println("\nInitializing SD card...");
  
  // Set chipSelect pin as OUTPUT
  pinMode(chipSelect, OUTPUT);
  digitalWrite(chipSelect, HIGH);

  // Attempt to initialize SD card
  if (!SD.begin(chipSelect)) {
    Serial.println("SD card initialization failed!");
    Serial.println("Possible causes:");
    Serial.println("- Incorrect chipSelect pin.");
    Serial.println("- Loose connections.");
    Serial.println("- Damaged SD card/module.");
    return;
  }

  Serial.println("SD card initialized successfully!");

  // Retrieve and print card information
  listFiles();
}

void listFiles() {
  File root = SD.open("/");
  Serial.println("\nListing files on SD card:");
  printDirectory(root, 0);
}

void printDirectory(File dir, int numTabs) {
  while (true) {
    File entry = dir.openNextFile();
    if (!entry) {
      // No more files
      break;
    }
    for (int i = 0; i < numTabs; i++) {
      Serial.print("\t");
    }
    Serial.print(entry.name());
    if (entry.isDirectory()) {
      Serial.println("/");
      printDirectory(entry, numTabs + 1);
    } else {
      // Files have sizes, print it
      Serial.print("\t\t");
      Serial.println(entry.size());
    }
    entry.close();
  }
}

// Function to clear the contents of a file
void clearFile(const char *filename) {
  if (SD.exists(filename)) {
    File myFile = SD.open(filename, O_WRITE | O_TRUNC); // Open with truncate mode
    if (myFile) {
      myFile.close(); // Closing the file ensures it's truncated to 0 bytes
      Serial.print(filename);
      Serial.println(" contents erased.");
    } else {
      Serial.print("Failed to open ");
      Serial.println(filename);
    }
  } else {
    Serial.print(filename);
    Serial.println(" does not exist.");
  }
}


// Function to read the contents of a file
void readFile(const char *filename) {
  if (SD.exists(filename)) {
    File myFile = SD.open(filename, FILE_READ);
    if (myFile) {
      Serial.print("Contents of ");
      Serial.println(filename);
      while (myFile.available()) {
        Serial.write(myFile.read()); // Print file content byte by byte
      }
      myFile.close();
      Serial.println("\nEnd of file.");
    } else {
      Serial.print("Failed to open ");
      Serial.println(filename);
    }
  } else {
    Serial.print(filename);
    Serial.println(" does not exist.");
  }
}


void loadPIDStateFromFile(float &integral, float &previousError) {
    myFile = SD.open("DEWLOG00.txt", FILE_READ);  // Open the file
    if (!myFile) {
        // File doesn't exist
        Serial.println("DEWLOG00.txt not found. Using default PID state.");
        integral = 0.0;  // Default value for integral
        previousError = 0.0;  // Default value for previousError
        return;
    }

    if (!myFile.available()) {
        // File exists but is empty
        Serial.println("DEWLOG00.txt is empty. Using default PID state.");
        integral = 0.0;  // Default value for integral
        previousError = 0.0;  // Default value for previousError
        myFile.close();
        return;
    }

    // File exists and has data, read the last line
    String lastLine;
    while (myFile.available()) {
        lastLine = myFile.readStringUntil('\n');  // Read until the last line
    }
    myFile.close();

    // Parse the last line to extract integral and previousError
    int lastCommaIndex = lastLine.lastIndexOf(',');
    int secondLastCommaIndex = lastLine.lastIndexOf(',', lastCommaIndex - 1);

    if (lastCommaIndex != -1 && secondLastCommaIndex != -1) {
        integral = lastLine.substring(secondLastCommaIndex + 1, lastCommaIndex).toFloat();
        previousError = lastLine.substring(lastCommaIndex + 1).toFloat();
        Serial.print("Loaded PID State: Integral = ");
        Serial.print(integral);
        Serial.print(", PreviousError = ");
        Serial.println(previousError);
    } else {
        // If parsing fails, fallback to defaults
        Serial.println("Error parsing DEWLOG00.txt for PID state. Using default values.");
        integral = 0.0;
        previousError = 0.0;
    }
}

Test Data

26.20, 21.00, 58.30, 17.35, 25.00, 65, -1.65, -54.19, -1.65
26.20, 20.75, 58.30, 17.35, 28.00, 73, -1.40, -75.00, -1.40
26.20, 20.94, 58.30, 17.35, 26.00, 67, -1.58, -75.00, -1.58
26.20, 20.94, 58.70, 17.46, 27.00, 70, -1.48, -75.00, -1.48
26.20, 20.88, 58.40, 17.38, 27.00, 69, -1.49, -75.00, -1.49
26.10, 20.81, 58.40, 17.29, 0.00, 0, -1.53, -63.75, -1.53
26.20, 20.75, 58.50, 17.41, 29.00, 75, -1.34, 0.00, -1.34
26.20, 20.81, 58.60, 17.43, 28.00, 73, -1.38, -75.00, -1.38
26.20, 20.69, 58.50, 17.41, 30.00, 77, -1.28, -75.00, -1.28
26.20, 20.75, 58.70, 17.46, 30.00, 77, -1.29, -75.00, -1.29
26.20, 20.81, 58.70, 17.46, 29.00, 74, -1.35, -75.00, -1.35
26.20, 20.75, 58.70, 17.46, 30.00, 77, -1.29, -75.00, -1.29
26.30, 20.75, 58.60, 17.53, 30.00, 79, -1.22, -75.00, -1.22
26.30, 20.69, 58.60, 17.53, 31.00, 81, -1.16, -75.00, -1.16
26.30, 20.75, 58.70, 17.55, 31.00, 80, -1.20, -75.00, -1.20
26.30, 20.75, 58.60, 17.53, 30.00, 79, -1.22, -75.00, -1.22
26.30, 20.88, 58.90, 17.61, 0.00, 0, -1.27, -63.75, -1.27
26.30, 20.88, 58.60, 17.53, 29.00, 74, -1.35, -54.19, -1.35
26.30, 20.88, 58.60, 17.53, 29.00, 74, -1.35, -75.00, -1.35
26.30, 20.75, 58.60, 17.53, 30.00, 79, -1.22, -75.00, -1.22
26.30, 20.81, 58.60, 17.53, 30.00, 77, -1.28, -75.00, -1.28
26.40, 20.81, 58.70, 17.65, 31.00, 81, -1.16, -75.00, -1.16
26.30, 20.81, 58.70, 17.55, 0.00, 0, -1.26, -63.75, -1.26
26.30, 20.69, 58.60, 17.53, 31.00, 81, -1.16, -54.19, -1.16
26.40, 20.75, 58.70, 17.65, 0.00, 0, -1.10, -46.06, -1.10
26.40, 20.62, 59.00, 17.73, 35.00, 91, -0.90, -39.15, -0.90
26.40, 20.62, 58.90, 17.70, 0.00, 0, -0.92, -33.28, -0.92
26.40, 20.56, 58.80, 17.68, 35.00, 91, -0.89, -28.29, -0.89
26.40, 20.44, 58.70, 17.65, 37.00, 95, -0.79, -75.00, -0.79
26.40, 20.38, 58.70, 17.65, 38.00, 97, -0.73, -75.00, -0.73
26.40, 20.56, 58.90, 17.70, 0.00, 0, -0.86, -63.75, -0.86

The dew controller could be very simply implemented starting from one of the examples in the Arduino PID library, with just a couple dozen lines of code and output to the serial monitor.

Then you need to learn how to tune the PID algorithm so that it actually does the job, that is, choose suitable values for Kp, Ki and Kd, which are generally unique to each system. To find tutorials on tuning, start with the search phrase "PID tuning".

Forum members will be happy to help once you have started on a sensible learning path, and you will end up understanding how the final product functions.

The problem with chatGPT is that it does not "think", it does not know the coding rules, and very often produces code with subtle or glaring errors that does not do what you want it to do. And in your case, with hundreds of lines of code that have nothing to do with the basic task. Forum members are very unlikely to help in such a situation.

What is decayRate meaning?
Is that 'eating' the integral windup?
A lag (dead time) is very difficult to control and can easily cause overshoots and oscillations.
Maybe PI control is better here...

Thanks, The decayRate decays the integral with each iteration, but even with that, it still saturates and stats that way for the most part.

What you last said is the key i was missing. I didn't realise that other types of control with less terms might be more suitable for this system, which has started me on a whole journey. Thanks! That's just the nudge I needed.

Re; Jremington, I get what you're saying about ChatGPT. A dew controller can be very simple i guess, unless you want it to display graphs on an OLED, and record the session to an SD. These are all things I couldn't have done without ChatGPT's help, and as a learning tool it's been invaluable. It's not perfect, but I ask it how to do something and it shows me how it could be done, using my own code as an example. i can run it and alter it, and learn what it does by example and build far more complex things that I could before in a far shorter period of time. It's a case of understanding following application. I know a ton more about programming than I did even a few weeks ago because of these tailored examples. Different people learn differently, I guess. But it's like having a tutor, albeit one that occasionally gets things wrong, but also, one that will self correct if questioned - I just didn't know what question I needed to ask - in this case, 'is PID the only control method for laggy thermal systems or would others be better?'. I only know of the existence of PID control because I asked ChatGPT a question about maintaining the temperature without oscillation. I guess what Im trying to say is all your points are correct, but for the way I learn, it's way better and faster than doing a course in programming or control theory. it would have taken years for me to be doing the stuff I'm currently doing. All I needed was the nudge that build_1971 provided. Thanks!

There are countless variants, and PID is itself three types of control rolled into one package. You might need only P, or PD, which will be determined when you get around to learning how to tune the PID constants.

Engineering students typically take a semester, or even an entire year of coursework generally titled Control Theory.

On the other hand, there many online PID tutorials that start with the basics and won't confuse you by throwing in unnecessary frills like "sigmoid for smoother control" and "integral decay", as did chatGPT.

1 Like

Your control needs an I action. Otherwise it will never reach the setpoint (unless you set P very high, but that will give instability).
So basically you need to set the decay rate to either zero or one to let I get smaller than -75.
You also could try larger P to make the offset smaller. And remove the deadband settings (set to 0) if you have pwm control for your heater. The deadband is used when you have both heating and cooling in place...
It would also be good to know the units of your parameters...
Pwm/degC?
Pwm/(degC.sec)?
Pwm/(degC/sec)?

75 degreeSeconds * 0.0001PWM/degreeSeconds=0.0075PWM doesn't look like it would have much effect.

And if it is hitting 75degreeSecond within ~2 minutes, that would make sense for ~1° error.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.