Trouble with using encoder and pid library together

Hi, I'm a relative beginner with arduino. I've been working on a project that will use the PID library by Brett Beauregard. I have a rotary encoder and over the last few days, I've built a menu system that operates through clicking and rotating. However, now that I'm adding the PID library, I find when adding the initial declaration PID myPID(&input, &output, &setpoint, 1.0, 1.0, 1.0, DIRECT ); the rotary encoder stops working. Well at least the switch on the encoder does, I don't know if the rotary part of it works or not because that only functions after clicking the encoder. Has anyone else come across anything like this? My code below:

// Include necessary libraries
#define ENCODER_DO_NOT_USE_INTERRUPTS
#include <LiquidCrystal.h>


#include <Encoder.h>
#include <EEPROM.h>
//#include <Wire.h>
#include <PID_v1.h>

// Define LCD pins
const int rs = 5, en = 4, d4 = 3, d5 = 2, d6 = 1, d7 = 0;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

// Thermistor constants
#define RT0 200000  // Ω
#define B 4045      // K
#define R 100000    // Ω (resistor used in voltage divider)



// Encoder pins
#define ENCODER_CLK 6
#define ENCODER_DT 7
#define ENCODER_SW 8
Encoder encoder(ENCODER_DT, ENCODER_CLK);

// Relay output pins
#define RELAY_VALVE 9
#define RELAY_HEATER 10

// Analog input pins
#define TEMP_PIN A0
#define PRES_PIN A1

// Control parameters
const uint8_t NUM_VARIABLES = 7;
const uint8_t NUM_VARIABLESI = 3;
const uint8_t NUM_VARIABLESF = 7;
const char* variableNames[NUM_VARIABLESF] = { "Temp-ramp", "Temp-hold", "Time", "Pressure", "P" , "I", "D" };
const float variableminf[NUM_VARIABLESF] = {50.0, 50.0, 10.0, 0.1, 0.0, 0.0, 0.0 };
const float variablemaxf[NUM_VARIABLESF] = {220.0, 220.0, 240.0, 2.0, 50.0, 50.0, 50.0 };
const float variablefdefault[NUM_VARIABLESF] = {130.0, 145.0, 45, 0.5, 5.0, 1.0, 1.0 };
float variablesf[NUM_VARIABLESF];  // temps, time, Pressure, Proportional, Integeral, and Derivative terms



const int EEPROM_START_ADDR = 0;  // EEPROM starting address

// encoder variables
enum Mode { IDLE, NAVIGATE, EDIT };
Mode currentMode = IDLE;
long lastEncoderPosNormalized = 0;
int currentMenuItem = 0;
double tempEditValueFloat = 0.0;  
bool buttonState = HIGH;
bool lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 25;
unsigned long pressStartTime = 0;
bool buttonPressedFlag = false;

// Program timer variables
unsigned long lastTempUpdate = 0;
unsigned long previousMillis = 0;
unsigned long programtimeseconds = 0;
unsigned long programtimeminutes = 0;
unsigned long programtimehours = 0;

// logic variables
bool started = false;  // flag for program started
bool steamvalveopen = false;

// variables for temperature measurement and pressure measurement
float RT, VR, ln, TX, T0, VRT;
float Pvoltage = 0.0;
float pressure = 0.0;
float maxpressure = 0.0;

double output, input, setpoint;
//PID* myPID;  // declare a pointer to a PID object
PID myPID(&input, &output, &setpoint, 1.0, 1.0, 1.0, DIRECT );
int WindowSize = 5000;
unsigned long windowStartTime;



void setup() {
  // Set pin modes
  pinMode(RELAY_HEATER, OUTPUT);
  pinMode(RELAY_VALVE, OUTPUT);
  pinMode(ENCODER_SW, INPUT_PULLUP);

  T0 = 25 + 273.15;  // Reference temp in Kelvin

  lcd.begin(16, 2);
  encoder.write(0);


  for (uint8_t i = 0; i < NUM_VARIABLESF; i++) {
  EEPROM.get(EEPROM_START_ADDR +  (i * sizeof(float)), variablesf[i]);
  if (isnan(variablesf[i]) || variablesf[i] < variableminf[i] || variablesf[i] > variablemaxf[i]) {
    variablesf[i] = variablefdefault[i];
  }
  }
  showIdle();
  
  windowStartTime = millis();

  //myPID = new PID(&input, &output, &setpoint, variablesf[4], variablesf[5], variablesf[6], DIRECT);
  //myPID.SetOutputLimits(0, WindowSize);
  //turn the PID on
  //myPID.SetMode(AUTOMATIC);
}

void loop() {
  // Temperature calculation
  VRT = (5.00 / 1023.00) * analogRead(A0);  // Voltage across thermistor
  VR = 5.00 - VRT;
  RT = VRT / (VR / R);                      // Resistance of thermistor
  ln = log(RT / RT0);                       // ln(RT/RT0)
  TX = (1 / ((ln / B) + (1 / T0))) - 273.15; // Temperature in Celsius

  // Pressure sensor conversion
  int sensorValue = analogRead(PRES_PIN);
  Pvoltage = sensorValue * (5.0 / 1023.0);
  pressure = 1.3953 * Pvoltage - 0.6977;  // Linear conversion formula
  
  
  // Program timer and heater/valve logic
  if (started && programtimeminutes < variablesf[2]) {  // program started and time running is less than the program length in minutes. set timer going
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= 1000) {
      previousMillis = currentMillis;
      programtimeseconds++;
      if (programtimeseconds > 59) {
        programtimeseconds = 0;
        programtimeminutes++;
      }
    }
    if (pressure > maxpressure) {  // Update maxpressure variable
      maxpressure = pressure;
    }
    
    if  ((maxpressure - pressure) > variablesf[3]) {  // turn on steam bleed valve to trap steam in vessel when pressure drops
      digitalWrite(RELAY_VALVE, HIGH);
      steamvalveopen = true;
    }
    input = TX;
    setpoint = variablesf[0];
    //myPID.Compute();
    if (millis() - windowStartTime > WindowSize){ 
      //time to shift the Relay Window
        windowStartTime += WindowSize;
      }
    if (output < millis() - windowStartTime) digitalWrite(RELAY_HEATER, HIGH);
    else digitalWrite(RELAY_HEATER, LOW);
    
    
    digitalWrite(RELAY_HEATER, HIGH);

  } else {  // program was cancelled or end of program was reached, turn heater off, open steam bleed valve, reset timer
    started = false;
    steamvalveopen = false;
    maxpressure = 0.0;
    digitalWrite(RELAY_VALVE, LOW);
    digitalWrite(RELAY_HEATER, LOW);
    programtimeseconds = 0;
    programtimeminutes = 0;
    programtimehours = 0;
  }

  readButton();
  handleEncoder();

  if (currentMode == IDLE && (millis() - lastTempUpdate > 1000)) {  //Update display every second
    lastTempUpdate = millis();
    updateIdleDisplay();
  }
}

// Read encoder pushbutton with debounce
void readButton() {
  int reading = digitalRead(ENCODER_SW);
  if (reading != lastButtonState) lastDebounceTime = millis();

  if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
    if (reading != buttonState) {
      buttonState = reading;
      if (buttonState == LOW) {
        pressStartTime = millis();
        buttonPressedFlag = false;
      } else {
        unsigned long pressDuration = millis() - pressStartTime;
        if (pressDuration >= 500) handleLongPress();
        else if (!buttonPressedFlag) handleShortPress();
      }
    }
  }
  lastButtonState = reading;
}

// Handle short press (edit variable or enter menu)
void handleShortPress() {
  if (currentMode == IDLE) {
    currentMode = NAVIGATE;
    currentMenuItem = 0;
    encoder.write(currentMenuItem * 4);
    lastEncoderPosNormalized = currentMenuItem;
    showNavigate();
  } else if (currentMode == NAVIGATE) {
  currentMode = EDIT;
  tempEditValueFloat = variablesf[currentMenuItem];

  if (currentMenuItem < NUM_VARIABLESI) {
    // First 3 variables: step size = 1
    encoder.write((int)(tempEditValueFloat) * 4);
    lastEncoderPosNormalized = (int)(tempEditValueFloat);
  } else {
    // Remaining variables: step size = 0.1
    encoder.write((int)(tempEditValueFloat * 10) * 4);
    lastEncoderPosNormalized = (int)(tempEditValueFloat * 10);
  }

    showEdit();
  } else if (currentMode == EDIT) {

    variablesf[currentMenuItem] = tempEditValueFloat;
    EEPROM.put(EEPROM_START_ADDR + currentMenuItem * sizeof(float), tempEditValueFloat);

    lcd.clear();
    lcd.print("Saved ");
    lcd.print(variableNames[currentMenuItem]);
    delay(800);

    currentMode = NAVIGATE;
    encoder.write(currentMenuItem * 4);
    lastEncoderPosNormalized = currentMenuItem;
    showNavigate();
  }
}

// Handle long press (cancel edit or toggle state)
void handleLongPress() {
  if (currentMode == EDIT) {
    currentMode = NAVIGATE;
    encoder.write(currentMenuItem * 4);
    lastEncoderPosNormalized = currentMenuItem;
    showNavigate();
  } else if (currentMode == NAVIGATE) {
    currentMode = IDLE;
    showIdle();
  } else if (currentMode == IDLE) {
    started = !started;
  }
}

// Handle encoder rotation
void handleEncoder() {
  long rawPos = encoder.read();
  long expectedRawPos = lastEncoderPosNormalized * 4;
  long deltaRaw = rawPos - expectedRawPos;

  // Filter large unexpected jumps (e.g., > 8 counts)
  if (abs(deltaRaw) > 8) {
    // Resynchronize encoder position to last known stable value
    encoder.write(expectedRawPos);
    return;
  }

  // Only process when a full step (4 counts) is detected
  if (deltaRaw >= 4 || deltaRaw <= -4) {
    // Determine direction: positive or negative step(s)
    int steps = deltaRaw / 4;  // Number of steps moved (can be >1 or < -1)

    if (currentMode == NAVIGATE) {
      int newMenuItem = currentMenuItem + steps;

      // Clamp newMenuItem within valid menu range
      if (newMenuItem < 0) newMenuItem = 0;
      if (newMenuItem >= NUM_VARIABLES) newMenuItem = NUM_VARIABLES - 1;

      if (newMenuItem != currentMenuItem) {
        currentMenuItem = newMenuItem;
        lastEncoderPosNormalized = currentMenuItem;
        encoder.write(currentMenuItem * 4);
        showNavigate();
      } else {
        // If no menu change, just resync encoder write
        encoder.write(lastEncoderPosNormalized * 4);
      }
    }else if (currentMode == EDIT) {
      int pos = lastEncoderPosNormalized + steps;

      if (currentMenuItem < NUM_VARIABLESI) {
        // Whole number steps
        int minPos = (int)variableminf[currentMenuItem];
        int maxPos = (int)variablemaxf[currentMenuItem];

        if (pos < minPos) pos = minPos;
        if (pos > maxPos) pos = maxPos;

        if (pos != lastEncoderPosNormalized) {
          lastEncoderPosNormalized = pos;
          tempEditValueFloat = (float)pos;
          encoder.write(pos * 4);
          showEdit();
        } else {
          encoder.write(lastEncoderPosNormalized * 4);
        }

      } else {
        // Decimal steps of 0.1
        int minPos = (int)(variableminf[currentMenuItem] * 10);
        int maxPos = (int)(variablemaxf[currentMenuItem] * 10);

        if (pos < minPos) pos = minPos;
        if (pos > maxPos) pos = maxPos;

        if (pos != lastEncoderPosNormalized) {
          lastEncoderPosNormalized = pos;
          tempEditValueFloat = pos / 10.0;
          encoder.write(pos * 4);
          showEdit();
        } else {
          encoder.write(lastEncoderPosNormalized * 4);
        }
      }
    }
  }
}

// Display idle screen with temperature and pressure
void showIdle() {
  lcd.clear();
  updateIdleDisplay();
}

// Update idle display every second
void updateIdleDisplay() {
  
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(TX, 1);
  lcd.print(" ");
  lcd.write(byte(223));  // Degree symbol
  lcd.print("C");

  char timeBuffer[6];
  sprintf(timeBuffer, "%3lu:%02lu", programtimeminutes, programtimeseconds);
  lcd.setCursor(10, 0);
  lcd.print(timeBuffer);

  lcd.setCursor(0, 1);
  lcd.print(pressure, 2);
  lcd.print(" Bar ");
  lcd.print(maxpressure, 2);
  
}

// Show variable navigation screen
void showNavigate() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(variableNames[currentMenuItem]);
  lcd.print(" = ");

  lcd.print(variablesf[currentMenuItem], 1);

  lcd.setCursor(0, 1);
  lcd.print("Click to edit");
}

// Show variable editing screen
void showEdit() {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print("Edit ");
  lcd.print(variableNames[currentMenuItem]);
  lcd.setCursor(0, 1);
  lcd.print("Value: ");
  lcd.print(tempEditValueFloat, 1);

}

You say you are a beginner but you choose to write your own switch handling code rather than using an established library. I will make a small wager I know where the bug(s) are.

I see you using the encoder as an OUTPUT device. I had no idea you could do that and no idea what will happen.

I had coded up a menu system using an encoder for a project earlier this year, but I ended up not using a menu at all. The code was much simpler and certainly no writing to it.

I will follow this thread with interest, maybe this old dog (83) can still learn a new trick.

Thank you for replying. Could you suggest an established library? To be very frank, I didn't write most of the code, ChatGPT played a heavy part in it. I'd love to be able to make the code simpler and more robust.

I think you're mistaken, it's not set up as an Output device. The two Pinmodes above the pinmode for the encoder switch are for a different part of the code and are nothing to do with the encoder

That isn't an encoder?

Here are the libraries I used. I think one is used by the other. I had a rotary encoder where I could read number of clicks CW or CCW, long press, click, double click.

#include <EncoderButton.h>

#include <Button2.h>

Button2.h will allow you to get rid of a lot of debounce code you have manually inserted in your sketch, now it will be simply ifPressed

Yes, apparently it’s writing to the internal position counter maintained by the Encoder library.

I really am grateful for any help on this. I'll look at the libraries you suggested. Thank you.

I added an lcd.print to the readButton() function.

With 'PID myPID(&input, &output, &setpoint, 1.0, 1.0, 1.0, DIRECT );' commented out, it prints 1 when the button isn't clicked, and 0 when it's clicked (seems correct.) If I uncomment the PID code, it produces the number 42949552. I can't work out why!

Edit: If I comment out handleShortPress() and handleLongPress() it produced the 1 and 0 again.

Code below:

void readButton() {
  int reading = digitalRead(ENCODER_SW);
  
  lcd.setCursor(8, 0);
  lcd.print(int(reading));  
  if (reading != lastButtonState) lastDebounceTime = millis();

  if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
    if (reading != buttonState) {
      buttonState = reading;
      if (buttonState == LOW) {
        pressStartTime = millis();
        buttonPressedFlag = false;
      } else {
        unsigned long pressDuration = millis() - pressStartTime;
        if (pressDuration >= 500) {
          handleLongPress();

        } else if (!buttonPressedFlag) {
          handleShortPress();
      
          buttonPressedFlag = true;  // Prevent repeated short press handling
        }
      }
    }
  }
  lastButtonState = reading;
}

It will be interesting to see what happens when you make use of the Button2 library.

This afternoon I looked at the code again, it seems the sprintf functions I was using for the lcd display was creating the problem. removing those has solved the problem. Btw, I've converted the code to use the ButtonEncoder library and it is a lot cleaner and simpler than what I originally had. However, it still didn't work properly when I added the PID library added and only after removing those sprintf functions did it start working. Thanks for the suggestion to use a library!

Your sprintf() statement is likely overflowing the timeBuffer array. Why did you only reserve 6 bytes?