• Models
  • Contests
  • Slicer
  • Login
  • Start Here
    thingiverse-iconprintables-iconcults3d-iconmakerworld-iconmyminifactory-icon

    3D GO

    3D ModelsContestsCollectionsSaved ModelsOn a mobile device?

3D GO

Privacy Policy
Pet Food Control 3D Printer File Image 1
Pet Food Control 3D Printer File Image 2
Pet Food Control 3D Printer File Image 3
Pet Food Control 3D Printer File Thumbnail 1
Pet Food Control 3D Printer File Thumbnail 2
Pet Food Control 3D Printer File Thumbnail 3

Pet Food Control

A2S_dESIGN avatarA2S_dESIGN

April 26, 2025

printables-icon
DescriptionCommentsTags

Description

1. Project Description

The smart feeder aims to automate your pet's feeding, providing access to food at specific times and only when the pet is present. This is achieved by integrating several electronic components controlled by an Arduino Uno.

Main Features:
  • Food Weighing: Measures the amount of food in the tank.
  • Presence Detection: Uses an ultrasonic sensor to detect the pet's proximity.
  • Automatic Lid Opening and Closing: An Arduino-controlled motor opens the lid when the pet is near and within the programmed time, and closes it when the pet leaves.
  • Schedule Control: Allows you to program time slots for the feeder to be opened.
  • User Interface: An LCD screen and a keypad allow you to interact with the device to set the time, feeding schedules, and perform other functions.

 

2. Supplies

2.1: 3D Printing Materials:
  • PETG (Polyethylene Terephthalate Glycol): Chosen for its strength, durability, and relative safety for food contact.
  • TPU (Thermoplastic Polyurethane) is a versatile thermoplastic elastomer that combines the properties of plastic and rubber.
    In this case, it was used to create the feet.
2.2:  Electronic Components

The electronic components required for the project are described below:

  • Arduino Uno: The microcontroller board that will act as the brain of the system, controlling all other components.
  • HC-SR04 Ultrasonic Sensor: Used to detect the pet's presence by measuring the distance.
  • Motor with L298N Driver: A direct current (DC) motor or servo motor (with the appropriate driver) will be used to open and close the lid. The L298N is a common driver for DC motors, allowing direction and speed control.
  • Two Limit Switches: These will be placed at the ends of the lid's path to indicate when it is fully open or closed, providing precise control of the motor's movement.
  • 2004 I2C LCD Display: A 20-character by 4-line display that will show the user information such as the current time, feeder status, and menu options. The I2C interface simplifies connection to Arduino.
  • HX711 Load Cell: A weight sensor that, together with the HX711 module (an amplifier and analog-to-digital converter), will measure the mass of the food in the tank.
  • Real-Time Clock (RTC) Module: A module like the DS3231 that maintains the current time even when the Arduino is powered off, thanks to an internal battery.
  • Keypad: A set of buttons with the following functions:
    • TARE: Zeroes the load cell reading.
    • MENU: Access the main options menu.
    • -: Decreases values in menus.
    • +: Increases values in menus.
  • Power supply 12v 3A: It allows you to transform electrical energy from ACV to DCV and can power different electronic devices.
  • Voltage regulator: DC Step Down voltage converter with LED display that allows input voltage to be reduced.
  • Dupont cables and connectors

 

3. Assembly Instructions

To complete this project, you will need the following tools:

  • Cyanoacrylate glue
  • Allen wrenches
  • Cutter
  • Sandpaper
  • Screwdriver

 

 Step 1:

  1. Print the parts.
  2. Cut and drill the holes in the composite base panel.

This panel is 4 mm thick to provide rigidity, as the load cell requires a base that will not flex under the weight.

  • Note: If you do not have a composite panel, you can fabricate it using other materials such as wood or aluminum, or even print it with your 3D printer.

 

Step 2: Load Cell Assembly

Items Required for this Step:

  • 4 M4 nuts
  • 2 M4x16 screws
  • 2 M4x20 screws
  • 4 M4x12 screws

Insert the 4 M4 nuts into their corresponding slots/recesses.

Mount the load cell onto its support.

Attach the support (with the load cell already mounted) to the composite base panel using the appropriate screws.

Route the load cell cables through the channel/conduit.

Attach the threaded base together with its spacer to the load cell.

Step 3: Gluing and assembling the left side structure

Items Required for this Step:

  • 7 M3 nuts
  • 4 DIN912 M3x12 screws
  • Cyanoacrylate glue

Insert the 7 M3 nuts into their corresponding slots/recesses in the side structure pieces.

Apply glue and join the "Lower Left Structure (Piece1)" piece to the "Lower Left Structure (Piece2)" piece.

Next, apply glue and attach the "FrontLeftGuide" piece.

Finally, apply glue and attach the "RearLeftGuide" piece.

Tip:

  • To hold the pieces firmly while the glue dries, you can use clamps (such as clothespins), as illustrated in the image
  •  

Once the parts of the left side structure are firmly glued and dry, mount this structure onto the composite base plate. Take the left leg (it is recommended to print this part in TPU) and assemble it to the structure/base assembly as shown in the photo.

 

Step 4: Gluing  the right side structure

Items Required for this Step:

  • 11 M3 nuts
  • 4 DIN912 M3x12 screws

Proceed with gluing in the same manner as in Step 3 (Gluing and mounting of the Left Side Structure), using the corresponding pieces for the right side.

Step 5:  Assembly of the Right Side Frame and Crossbars

Items Required for this Step:

  • 2 M3 nuts
  • 4 DIN 912 M3x12 screws
  • 2 DIN 912 M3x8 screws
  • 2 DIN 912 M3x6 screws

Assembly Procedure:

Once the right side structure is firmly glued and dry, first mount the "Lower Rear Bar" piece. 

Subsequently, assemble the right side structure together with the leg onto the composite base plate, securing it in place. 

Then, join the upper part of both side structures (left and right) using the "Upper Rear Bar" piece.

Tip:

  • Ensure that both side structures are parallel and correctly aligned before firmly attaching the crossbars.

 

Step 6: Mounting Electronic Components and Arduino

  • 16 M3 nuts
  • 8 DIN 912 M3x16 screws
  • 7 DIN 912 M3x8 screws

Assembly Procedure:

Insert the M3 nuts into their slots. Make sure they are properly seated.

Place the Arduino Uno onto its corresponding holder and secure it using the screws. Likewise, mount the DS3231 module.

Place the holders onto the side structure, leaving a free space. As shown in the photo.

Once the three holders are mounted, attach the HX711 module to its holder.

 

Step 7: Engine assembly

2 M3 nuts
2 DIN 912 M3x25
1 DIN 912 M3x16
4 DIN 912 M3x12
1 DIN 912 M3x8
2 DIN M3 M3x6
1 608ZZ bearing

 

Step 8:

(in edition)

4. C++ code

4.1: Code to program
#include <Wire.h>              // For I2C communication
#include <RTClib.h>            // For DS3231 RTC
#include <LiquidCrystal_I2C.h> // For I2C LCD
#include <HX711_ADC.h>         // For HX711 load cell amplifier
#include <EEPROM.h>            // For non-volatile memory

// --- I2C Configuration ---
const uint8_t LCD_ADDRESS = 0x27; // Common I2C Address for LCD (might be 0x3F or other)
const uint8_t RTC_ADDRESS = 0x68; // Standard I2C Address for DS3231

// --- Object Initialization ---
RTC_DS3231 rtc;                     // Object for DS3231 RTC
LiquidCrystal_I2C lcd(LCD_ADDRESS, 20, 4); // Object for 20x4 LCD (address, columns, rows)

// --- Pin Definitions ---
const int TRIG_PIN = 2;             // Ultrasonic sensor Trigger pin
const int ECHO_PIN = 3;             // Ultrasonic sensor Echo pin
const int MOTOR_IN1 = 7;            // Motor control pin 1
const int MOTOR_IN2 = 6;            // Motor control pin 2
const int OPEN_LIMIT_SWITCH = 8;    // Limit switch for fully open door
const int CLOSE_LIMIT_SWITCH = 9;   // Limit switch for fully closed door
const int HX711_DOUT_PIN = 4;       // HX711 Data Out pin (DT)
const int HX711_SCK_PIN = 5;        // HX711 Serial Clock pin (SCK)
// Button Pins
const int TARE_BUTTON_PIN = 12;     // Button to tare the scale
const int MODE_BUTTON_PIN = 13;     // Button to enter/cycle settings mode
const int UP_BUTTON_PIN = 11;       // Button to increment value in settings
const int DOWN_BUTTON_PIN = 10;     // Button to decrement value in settings

// --- Configuration Constants ---
const unsigned long PRESENCE_TIMEOUT = 15000; // ms: Time door stays open after last presence
const int DISTANCE_THRESHOLD = 40;            // cm: Distance to detect presence/obstacle
const int SENSOR_READ_INTERVAL = 100;         // ms: How often to read sensors
const int MOTOR_START_DELAY = 50;             // ms: Small delay after starting motor
const int LCD_UPDATE_INTERVAL = 500;          // ms: How often to update LCD
const unsigned long DEBOUNCE_DELAY = 50;      // ms: Debounce time for buttons
const unsigned long MANUAL_OVERRIDE_HOLD_TIME = 1000; // ms: Hold time for manual override buttons
const unsigned long MOTOR_TIMEOUT_DURATION = 6000;  // 6 seconds motor run timeout

// --- Weight Unit Enum ---
enum WeightUnit { UNIT_GRAMS, UNIT_OUNCES };

// --- Settings Variables (loaded from EEPROM, with defaults) ---
int restrictedHourStart = 22; // Default restriction start time (10 PM)
int restrictedHourEnd = 6;    // Default restriction end time (6 AM)
WeightUnit currentUnit = UNIT_GRAMS; // Default weight unit

// --- HX711 Calibration ---
// !!! REPLACE THIS VALUE WITH YOUR OWN OBTAINED FROM CALIBRATION (using grams) !!!
const float CALIBRATION_FACTOR = 419.0; // Example value - MUST BE CHANGED!

// --- EEPROM Addresses & Check Value ---
#define EEPROM_ADDR_R_START 0
#define EEPROM_ADDR_R_END   (EEPROM_ADDR_R_START + sizeof(int))
#define EEPROM_ADDR_UNIT    (EEPROM_ADDR_R_END + sizeof(int))
#define EEPROM_ADDR_CHECK   (EEPROM_ADDR_UNIT + sizeof(WeightUnit))
#define EEPROM_CHECK_VALUE  0x5B // Use a consistent check value

// --- HX711 Object Initialization ---
HX711_ADC LoadCell(HX711_DOUT_PIN, HX711_SCK_PIN);

// --- System States (Door) ---
enum State {
  INIT_OPENING, INIT_CLOSING, CLOSED, OPENING, OPEN, CLOSING, OBSTACLE_DETECTED,
  ERROR_MOTOR_TIMEOUT / Error State
};
State currentState = CLOSED;

// --- Global Variables ---
unsigned long lastPresenceTime = 0;
unsigned long lastSensorReadTime = 0;
unsigned long lastLcdUpdateTime = 0;
long currentDistance = -1;
bool openLimitReached = false;
bool closeLimitReached = false;
DateTime now;
float currentWeight = 0.0; // Stores weight in GRAMS
bool hx711_ready = false;
// Settings Mode
bool isInSettingsMode = false;
enum SettingField { NONE, SET_HOUR, SET_MINUTE, SET_RESTRICT_START, SET_RESTRICT_END, SET_UNIT }; // Uses only fields from arduinoIngles.TXT
SettingField currentSettingField = NONE;
int tempHour = 0; int tempMinute = 0; int tempR_Start = 0; int tempR_End = 0; WeightUnit tempUnit = UNIT_GRAMS;
// Button Debounce
bool lastTareState = HIGH; unsigned long lastTareDebounceTime = 0;
bool lastModeState = HIGH; unsigned long lastModeDebounceTime = 0;
bool lastUpState = HIGH;   unsigned long lastUpDebounceTime = 0;
bool lastDownState = HIGH; unsigned long lastDownDebounceTime = 0;
bool stableTareState = HIGH; bool stableModeState = HIGH; bool stableUpState = HIGH; bool stableDownState = HIGH;
bool tarePressed = false; bool modePressed = false; bool upPressed = false; bool downPressed = false;
// Long Press
unsigned long upButtonPressStartTime = 0;
unsigned long downButtonPressStartTime = 0;
// Motor Timeout
unsigned long motorStartTime = 0; // Timestamp when motor starts

// --- Function Prototypes ---
long getDistance();
void openDoor(); void closeDoor(); void stopMotor();
void readLimitSwitches(); void handleStateMachine(); bool isRestrictedTimeNow();
void updateLcdDisplay(); const char* getStateString(State s);
void readWeight(); void readButtons(); void handleSettingsMode(); void tareScale();
const char* getSettingFieldString(SettingField sf);
void loadConfigFromEEPROM(); void saveConfigToEEPROM();
const char* getUnitString(WeightUnit u);

// --- Initial Setup ---
void setup() {
  Serial.begin(9600);
  Serial.println(F("Starting System V1.1 (Motor Timeout Added)...")); // Version update

  Wire.begin();

  // Init LCD
  lcd.init(); lcd.backlight(); lcd.setCursor(0, 0); lcd.print(F("Initializing Sys..."));
  Serial.println(F("LCD initialized."));

  // Init RTC
  if (!rtc.begin()) { Serial.println(F("Error: DS3231 RTC!")); lcd.setCursor(0, 1); lcd.print(F("Error: RTC missing")); while (1) delay(10); }
  Serial.println(F("DS3231 RTC found."));
  if (rtc.lostPower()) { Serial.println(F("RTC lost power!")); lcd.setCursor(0, 1); lcd.print(F("RTC: Set Time")); /* rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); */ }
  lcd.setCursor(0, 1); lcd.print(F("RTC OK")); delay(500); lcd.setCursor(0,1); lcd.print(F("                  "));

  // Init HX711
  Serial.println(F("Initializing HX711...")); lcd.setCursor(0,1); lcd.print(F("HX711 Init..."));
  LoadCell.begin(); LoadCell.start(2000, true);
  if (LoadCell.getTareTimeoutFlag()) { Serial.println(F("HX711 tare timeout.")); lcd.setCursor(0,1); lcd.print(F("Error: HX711 Tare TO")); }
  else { LoadCell.setCalFactor(CALIBRATION_FACTOR); Serial.println(F("HX711 ready.")); lcd.setCursor(0,1); lcd.print(F("HX711 OK")); hx711_ready = true; }
  delay(500); lcd.setCursor(0,1); lcd.print(F("                  "));

  // Configure Pins
  pinMode(TRIG_PIN, OUTPUT); pinMode(ECHO_PIN, INPUT); pinMode(MOTOR_IN1, OUTPUT); pinMode(MOTOR_IN2, OUTPUT);
  pinMode(OPEN_LIMIT_SWITCH, INPUT_PULLUP); pinMode(CLOSE_LIMIT_SWITCH, INPUT_PULLUP);
  pinMode(TARE_BUTTON_PIN, INPUT_PULLUP); pinMode(MODE_BUTTON_PIN, INPUT_PULLUP); pinMode(UP_BUTTON_PIN, INPUT_PULLUP); pinMode(DOWN_BUTTON_PIN, INPUT_PULLUP);

  // Initialize button states
  stableTareState=digitalRead(TARE_BUTTON_PIN); stableModeState=digitalRead(MODE_BUTTON_PIN); stableUpState=digitalRead(UP_BUTTON_PIN); stableDownState=digitalRead(DOWN_BUTTON_PIN);
  lastTareState=stableTareState; lastModeState=stableModeState; lastUpState=stableUpState; lastDownState=stableDownState;

  // Load config from EEPROM
  loadConfigFromEEPROM();

  stopMotor(); // Ensure motor is stopped initially
  readLimitSwitches(); // Read initial limit switch states

  // Determine initial door state based on limit switches
  if (closeLimitReached) { currentState = CLOSED; Serial.println(F("Initial state: CLOSED")); }
  else if (openLimitReached) { currentState = OPEN; Serial.println(F("Initial state: OPEN")); }
  else { currentState = INIT_OPENING; openDoor(); /* motorStartTime set in openDoor() */ Serial.println(F("Initial state: INIT_OPENING")); }

  updateLcdDisplay();
} // End setup


// --- Main Loop ---
void loop() {
  now = rtc.now();
  readButtons();

  if (isInSettingsMode) {
    handleSettingsMode();
  } else if (currentState == ERROR_MOTOR_TIMEOUT) {
      // In error state, do nothing except update LCD (already handled below)
      // Optionally, check for MODE button press to reset?
      // if (modePressed) {
      //    Serial.println(F("Motor Timeout acknowledged. Resetting to CLOSED."));
      //    currentState = CLOSED;
      // }
      ; // Do nothing for now
  }
   else {
    // --- Normal Operation Mode ---
    unsigned long currentMillis = millis();
    if (tarePressed) { tareScale(); }

    // Handle Manual Override
    bool override_action_taken = false;
    if (isRestrictedTimeNow() && currentState == CLOSED && stableUpState == LOW) {
       if (upButtonPressStartTime != 0 && (currentMillis - upButtonPressStartTime > MANUAL_OVERRIDE_HOLD_TIME)) {
         Serial.println(F("OVERRIDE: Opening door (UP long press)..."));
         currentState = OPENING; openDoor(); // motorStartTime set inside openDoor
         delay(MOTOR_START_DELAY); // Let motor start
         upButtonPressStartTime = 0; override_action_taken = true;
       }
    } else if (isRestrictedTimeNow() && currentState == OPEN && stableDownState == LOW) {
        if (downButtonPressStartTime != 0 && (currentMillis - downButtonPressStartTime > MANUAL_OVERRIDE_HOLD_TIME)) {
            Serial.println(F("OVERRIDE: Closing door (DOWN long press)..."));
            currentState = CLOSING; closeDoor(); // motorStartTime set inside closeDoor
            delay(MOTOR_START_DELAY); // Let motor start
            downButtonPressStartTime = 0; override_action_taken = true;
        }
     }

    // Read Sensors (if no override)
    if (!override_action_taken) {
       if (currentMillis - lastSensorReadTime >= SENSOR_READ_INTERVAL) {
         currentDistance = getDistance();
         readLimitSwitches();
         readWeight();
         lastSensorReadTime = currentMillis;
       }
    } else { lastSensorReadTime = currentMillis; }

    // Handle State Machine
    handleStateMachine();

    // Check for entering settings mode
    if (modePressed) {
      isInSettingsMode = true; currentSettingField = SET_HOUR;
      tempHour = now.hour(); tempMinute = now.minute(); tempR_Start = restrictedHourStart; tempR_End = restrictedHourEnd; tempUnit = currentUnit;
      Serial.println(F("Entering Settings Mode..."));
      stopMotor(); // Stop motor and reset timer when entering settings
      updateLcdDisplay(); lastLcdUpdateTime = currentMillis;
    }
  } // End Normal Mode

  // Update LCD periodically
  if (millis() - lastLcdUpdateTime >= LCD_UPDATE_INTERVAL) {
    updateLcdDisplay(); lastLcdUpdateTime = millis();
  }
} // End loop


// --- Function Implementations ---

// Reads buttons, handles debounce, sets flags, tracks long press start times
void readButtons() {
  tarePressed = false; modePressed = false; upPressed = false; downPressed = false;
  unsigned long now_ms = millis();

  // --- TARE ---
  bool rT = digitalRead(TARE_BUTTON_PIN);
  if (rT != lastTareState) { lastTareDebounceTime = now_ms; }
  if ((now_ms - lastTareDebounceTime) > DEBOUNCE_DELAY) { if (rT != stableTareState) { stableTareState = rT; if (stableTareState == LOW) { tarePressed = true; Serial.println(F(">>> TARE Pressed <<<")); } } }
  lastTareState = rT;

  // --- MODE ---
  bool rM = digitalRead(MODE_BUTTON_PIN);
  if (rM != lastModeState) { lastModeDebounceTime = now_ms; }
  if ((now_ms - lastModeDebounceTime) > DEBOUNCE_DELAY) { if (rM != stableModeState) { stableModeState = rM; if (stableModeState == LOW) { modePressed = true; Serial.println(F(">>> MODE Pressed <<<")); } } }
  lastModeState = rM;

  // --- UP ---
  bool rU = digitalRead(UP_BUTTON_PIN);
  if (rU != lastUpState) { lastUpDebounceTime = now_ms; }
  if ((now_ms - lastUpDebounceTime) > DEBOUNCE_DELAY) { if (rU != stableUpState) { stableUpState = rU; if (stableUpState == LOW) { upPressed = true; Serial.println(F(">>> UP Pressed <<<")); upButtonPressStartTime = now_ms; } else { upButtonPressStartTime = 0; } } }
  lastUpState = rU;

  // --- DOWN ---
  bool rD = digitalRead(DOWN_BUTTON_PIN);
  if (rD != lastDownState) { lastDownDebounceTime = now_ms; }
  if ((now_ms - lastDownDebounceTime) > DEBOUNCE_DELAY) { if (rD != stableDownState) { stableDownState = rD; if (stableDownState == LOW) { downPressed = true; Serial.println(F(">>> DOWN Pressed <<<")); downButtonPressStartTime = now_ms; } else { downButtonPressStartTime = 0; } } }
  lastDownState = rD;
}


// Performs tare operation
void tareScale() {
   if (!hx711_ready) { Serial.println(F("HX711 not ready")); return; }
   Serial.println(F("Taring scale..."));
   lcd.setCursor(0, 0); lcd.print(F("Taring...           "));
   LoadCell.tare(); currentWeight = 0.0; delay(100);
   updateLcdDisplay(); lastLcdUpdateTime = millis();
   Serial.println(F("Scale tared."));
}


// Handles settings menu navigation and value changes
void handleSettingsMode() {
  // MODE Button: Cycle or Save/Exit
  if (modePressed) {
    // Cycle to the next setting field
    int currentIdx = (int)currentSettingField;
    int nextIdx = currentIdx + 1;
    // Cycle from last field (SET_UNIT) back to first (SET_HOUR) or exit (NONE)?
    // Let's exit after last field.
    if (nextIdx > SET_UNIT) { // If currently on last item (SET_UNIT)
        // --- Save and Exit ---
        Serial.println(F("Saving configuration and exiting..."));
        now = rtc.now();
        rtc.adjust(DateTime(now.year(), now.month(), now.day(), tempHour, tempMinute, now.second()));
        restrictedHourStart = tempR_Start; restrictedHourEnd = tempR_End;
        currentUnit = tempUnit;
        saveConfigToEEPROM(); // Save relevant settings
        currentSettingField = NONE;
        isInSettingsMode = false;
        Serial.println(F("Exited Settings Mode."));
        // No updateLcdDisplay here, let the main loop handle it
        return; // Exit function immediately
    } else {
        // Move to next field
        currentSettingField = (SettingField)nextIdx;
        Serial.print(F("New field selected: ")); Serial.println(getSettingFieldString(currentSettingField));
        updateLcdDisplay(); lastLcdUpdateTime = millis(); // Update LCD immediately
    }
  } // End if modePressed

  // UP/DOWN Buttons: Modify temporary value
  if (upPressed || downPressed) {
    int delta = upPressed ? 1 : -1;
    bool valueChanged = true;
    switch (currentSettingField) {
      case SET_HOUR: tempHour = (tempHour + delta + 24) % 24; break;
      case SET_MINUTE: tempMinute = (tempMinute + delta + 60) % 60; break;
      case SET_RESTRICT_START: tempR_Start = (tempR_Start + delta + 24) % 24; break;
      case SET_RESTRICT_END: tempR_End = (tempR_End + delta + 24) % 24; break;
      case SET_UNIT: tempUnit = (tempUnit == UNIT_GRAMS) ? UNIT_OUNCES : UNIT_GRAMS; break; // Toggle
      default: valueChanged = false; break; // No action if NONE
    }
    if (valueChanged) {
        Serial.print(F(" Temp value updated for: ")); Serial.println(getSettingFieldString(currentSettingField));
        updateLcdDisplay(); lastLcdUpdateTime = millis(); // Update LCD immediately
    }
  }
}


// Updates the LCD display based on current mode (Normal or Settings)
void updateLcdDisplay() {
  char lineBuffer[21], floatBuffer[10];
  const float G_TO_OZ_FACTOR = 0.035274;

  // *** NEW: Handle ERROR_MOTOR_TIMEOUT state display ***
  if (currentState == ERROR_MOTOR_TIMEOUT) {
      lcd.clear();
      lcd.setCursor(0, 0); lcd.print(F("!!! MOTOR ERROR !!! "));
      lcd.setCursor(0, 1); lcd.print(F(" Movement Timeout   "));
      lcd.setCursor(0, 2); lcd.print(F(" Check Door / Limit "));
      lcd.setCursor(0, 3); lcd.print(F(" Switches!          "));
      return; // Stop further display updates in error state
  }

  lcd.clear(); // Clear display for normal/settings redraw

  if (isInSettingsMode) {
    // --- Settings Mode Interface --- (Simplified, No Scroll)
    lcd.setCursor(0, 0); lcd.print(F("SETUP:")); lcd.print(getSettingFieldString(currentSettingField));

    lcd.setCursor(0, 1); snprintf(lineBuffer, sizeof(lineBuffer), "Time:   %02d:%02d %c", tempHour, tempMinute, (currentSettingField == SET_HOUR || currentSettingField == SET_MINUTE) ? '<' : ' '); lcd.print(lineBuffer);
    lcd.setCursor(0, 2); snprintf(lineBuffer, sizeof(lineBuffer), "Restr:  %02d - %02d %c", tempR_Start, tempR_End, (currentSettingField == SET_RESTRICT_START || currentSettingField == SET_RESTRICT_END) ? '<' : ' '); lcd.print(lineBuffer);
    lcd.setCursor(0, 3); snprintf(lineBuffer, sizeof(lineBuffer), "Unit: %-7s %c", getUnitString(tempUnit), (currentSettingField == SET_UNIT) ? '<' : ' '); lcd.print(lineBuffer);

  } else {
    // --- Normal Operation Interface ---
    // Line 0: Weight
    lcd.setCursor(0, 0);
    if (hx711_ready) { float dW=currentWeight; const char* uS=" g"; int p=1; if(currentUnit==UNIT_OUNCES){dW*=G_TO_OZ_FACTOR; uS=" oz"; p=2;} dtostrf(dW,1,p,floatBuffer); snprintf(lineBuffer,sizeof(lineBuffer),"Weight:%-7s%s",floatBuffer,uS); }
    else { snprintf(lineBuffer, sizeof(lineBuffer), "Weight: ---.-     "); }
    lcd.print(lineBuffer); for (int i=strlen(lineBuffer); i<20; i++){lcd.print(F(" "));}

    // Line 1: Time
    snprintf(lineBuffer,sizeof(lineBuffer),"Time: %02d:%02d:%02d",now.hour(),now.minute(),now.second()); lcd.setCursor(0,1); lcd.print(lineBuffer); for(int i=strlen(lineBuffer); i<20; i++){lcd.print(F(" "));}

    // Line 2: Status
    snprintf(lineBuffer, sizeof(lineBuffer), "Status: %-12s", getStateString(currentState)); lcd.setCursor(0, 2); lcd.print(lineBuffer); for (int i = strlen(lineBuffer); i < 20; i++) { lcd.print(F(" ")); }

    // Line 3: Dist/Restr
    lcd.setCursor(0,3); if(isRestrictedTimeNow()){snprintf(lineBuffer,sizeof(lineBuffer),"Restr: %02d-%02dh ACTIVE", restrictedHourStart, restrictedHourEnd);}else{if(currentDistance>=0 && currentDistance<999){snprintf(lineBuffer,sizeof(lineBuffer),"Dist: %3ld cm      ",currentDistance);}else{snprintf(lineBuffer,sizeof(lineBuffer),"Dist: --- cm      ");}} lcd.print(lineBuffer); for(int i=strlen(lineBuffer); i<20; i++){lcd.print(F(" "));}
  }
}


// Converts SettingField enum to a printable string (English, No F() in return)
const char* getSettingFieldString(SettingField sf) {
  switch (sf) { case SET_HOUR: return "HOUR"; case SET_MINUTE: return "MINUTE"; case SET_RESTRICT_START: return "R.START"; case SET_RESTRICT_END: return "R.END"; case SET_UNIT: return "UNIT"; case NONE: return " "; default: return "?"; }
}

// Converts WeightUnit enum to a printable string (English)
const char* getUnitString(WeightUnit u) {
  return (u == UNIT_GRAMS) ? "GRAMS" : "OUNCES";
}

// Reads weight from HX711
void readWeight() { if (hx711_ready) { if (LoadCell.update()) { currentWeight = LoadCell.getData(); } } }

// Reads limit switches
void readLimitSwitches() { openLimitReached=(digitalRead(OPEN_LIMIT_SWITCH)==LOW); closeLimitReached=(digitalRead(CLOSE_LIMIT_SWITCH)==LOW); }

// Checks if current time is within restricted period
bool isRestrictedTimeNow() { int cH=now.hour(); bool r=false; if(restrictedHourStart > restrictedHourEnd){r=(cH>=restrictedHourStart || cH<restrictedHourEnd);}else{if(restrictedHourStart == restrictedHourEnd) return false; r=(cH>=restrictedHourStart && cH<restrictedHourEnd);} return r; }


// Main state machine for door operation
void handleStateMachine() {
 // Don't run state machine if in settings or error state
 if (isInSettingsMode || currentState == ERROR_MOTOR_TIMEOUT) return;

 State previousState = currentState;

 // --- Motor Timeout Check ---
 // Check only if motor is supposed to be moving and timer is running
 if (motorStartTime != 0 && (currentState == OPENING || currentState == CLOSING || currentState == INIT_OPENING || currentState == INIT_CLOSING)) {
    if (millis() - motorStartTime > MOTOR_TIMEOUT_DURATION) {
        Serial.println(F("!!! MOTOR TIMEOUT !!! State was: ")); Serial.println(getStateString(currentState));
        stopMotor(); // This will also set motorStartTime = 0
        currentState = ERROR_MOTOR_TIMEOUT;
        // Update LCD immediately to show error
        updateLcdDisplay();
        lastLcdUpdateTime = millis();
        return; // Exit state machine handling for this cycle
    }
 }

 // --- Normal State Transitions ---
 switch (currentState) {
    case INIT_OPENING:
      if (openLimitReached) { Serial.println(F("INIT: Open Limit")); stopMotor(); currentState=INIT_CLOSING; closeDoor(); /* timer started in closeDoor */ }
      break;
    case INIT_CLOSING:
      if (closeLimitReached) { Serial.println(F("INIT: Close Limit")); stopMotor(); currentState=CLOSED; }
      break;
    case CLOSED:
      if (currentDistance>=0 && currentDistance<DISTANCE_THRESHOLD) {
        if (!isRestrictedTimeNow()) { Serial.println(F("CLOSED: Presence, opening")); currentState=OPENING; openDoor(); /* timer started in openDoor */ }
        else { /*Serial.println(F("CLOSED: Presence, restricted"));*/ }
      }
      break;
    case OPENING:
      if (openLimitReached) { Serial.println(F("OPENING: Limit Reached")); stopMotor(); currentState=OPEN; }
      break;
    case OPEN:
      if (currentDistance>=0 && currentDistance<DISTANCE_THRESHOLD) { lastPresenceTime=millis(); }
      if (millis()-lastPresenceTime >= PRESENCE_TIMEOUT) { Serial.println(F("OPEN: Timeout, closing")); currentState=CLOSING; closeDoor(); /* timer started in closeDoor */ }
      break;
    case CLOSING:
      if (currentDistance>=0 && currentDistance<DISTANCE_THRESHOLD) { Serial.println(F("CLOSING: Obstacle!")); stopMotor(); currentState=OBSTACLE_DETECTED; openDoor(); /* timer started in openDoor */ }
      else if (closeLimitReached) { Serial.println(F("CLOSING: Limit Reached")); stopMotor(); currentState=CLOSED; }
      break;
    case OBSTACLE_DETECTED: // Re-opening after obstacle
      if (openLimitReached) { Serial.println(F("OBSTACLE: Re-opened")); stopMotor(); currentState=OPEN; lastPresenceTime=millis(); }
      break;
    // ERROR_MOTOR_TIMEOUT case intentionally empty here - handled at start of loop/function
 }

 // If the state changed (and it wasn't to the error state already handled), update LCD
 if (currentState != previousState) {
     Serial.print(F("State Change: ")); Serial.println(getStateString(currentState));
     updateLcdDisplay();
     lastLcdUpdateTime = millis();
 }
}


// Converts State enum to a pr
const char* getStateString(State s) {
 switch(s){
    case INIT_OPENING: return "Init Opening"; case INIT_CLOSING: return "Init Closing";
    case CLOSED: return "Closed"; case OPENING: return "Opening"; case OPEN: return "Open";
    case CLOSING: return "Closing"; case OBSTACLE_DETECTED: return "Obstacle!";
    case ERROR_MOTOR_TIMEOUT: return "ERROR TIMEOUT";
    default: return "Unknown";
 }
}


// Measures distance using ultrasonic sensor
long getDistance() {
 digitalWrite(TRIG_PIN,LOW);delayMicroseconds(2);digitalWrite(TRIG_PIN,HIGH);delayMicroseconds(10);digitalWrite(TRIG_PIN,LOW); long d=pulseIn(ECHO_PIN,HIGH,25000); return (d==0) ? 999 : d*0.0343/2.0;
}


// Motor control functions - Added motorStartTime handling
void openDoor() {
  digitalWrite(MOTOR_IN1,HIGH); digitalWrite(MOTOR_IN2,LOW);
  motorStartTime = millis(); // <<< Start timer
  Serial.println(F("Motor: Opening"));
}
void closeDoor() {
  digitalWrite(MOTOR_IN1,LOW); digitalWrite(MOTOR_IN2,HIGH);
  motorStartTime = millis(); // <<< Start timer
  Serial.println(F("Motor: Closing"));
}
void stopMotor() {
  digitalWrite(MOTOR_IN1,LOW); digitalWrite(MOTOR_IN2,LOW);
  motorStartTime = 0; // <<< Stop timer
  Serial.println(F("Motor: Stopped"));
}


// Loads configuration from EEPROM
void loadConfigFromEEPROM() {
  Serial.println(F("Loading config from EEPROM..."));
  byte checkValue = EEPROM.read(EEPROM_ADDR_CHECK);

  if (checkValue == EEPROM_CHECK_VALUE) {
    EEPROM.get(EEPROM_ADDR_R_START, restrictedHourStart);
    EEPROM.get(EEPROM_ADDR_R_END, restrictedHourEnd);
    EEPROM.get(EEPROM_ADDR_UNIT, currentUnit);
    if (currentUnit != UNIT_GRAMS && currentUnit != UNIT_OUNCES) { currentUnit = UNIT_GRAMS; Serial.println(F("Warn: Bad unit"));}
    Serial.println(F("Configuration loaded."));
  } else {
    Serial.println(F("EEPROM invalid. Saving defaults."));
    restrictedHourStart = 22; restrictedHourEnd = 6; currentUnit = UNIT_GRAMS; // Ensure defaults
    saveConfigToEEPROM(); // Use save function
    EEPROM.write(EEPROM_ADDR_CHECK, EEPROM_CHECK_VALUE);
    Serial.println(F("Default values saved."));
  }
  Serial.print(F("  Restr Start: ")); Serial.println(restrictedHourStart);
  Serial.print(F("  Restr End: ")); Serial.println(restrictedHourEnd);
  Serial.print(F("  Weight Unit: ")); Serial.println(getUnitString(currentUnit));
}


// Saves configuration to EEPROM
void saveConfigToEEPROM() {
  Serial.println(F("Saving config to EEPROM..."));
  EEPROM.put(EEPROM_ADDR_R_START, restrictedHourStart);
  EEPROM.put(EEPROM_ADDR_R_END, restrictedHourEnd);
  EEPROM.put(EEPROM_ADDR_UNIT, currentUnit);
  Serial.println(F("Configuration saved."));
}
4.2: Detailed step-by-step explanation of the Arduino code.
#include <Wire.h>              // For I2C communication
#include <RTClib.h>            // For DS3231 RTC
#include <LiquidCrystal_I2C.h> // For I2C LCD
#include <HX711_ADC.h>         // For HX711 load cell amplifier
#include <EEPROM.h>            // For non-volatile memory

Includes (#include ...): These lines bring in the necessary libraries for the code to function.

  • Wire.h: Essential for I2C (Inter-Integrated Circuit) communication, used to talk to the LCD and RTC.
  • RTClib.h: Specific library for controlling the DS3231 Real-Time Clock (RTC) module. Allows getting the current date and time.
  • LiquidCrystal_I2C.h: Library for controlling LCD screens connected via I2C. Makes it easier to write text to the display.
  • HX711_ADC.h: Library for interacting with the HX711 amplifier, which reads data from the load cell (for weight measurement).
  • EEPROM.h: Allows reading from and writing data to the Arduino's EEPROM memory. This memory retains data even when power is removed, useful for saving settings.
// --- I2C Configuration ---
const uint8_t LCD_ADDRESS = 0x27; // Common I2C Address for LCD (might be 0x3F or other)
const uint8_t RTC_ADDRESS = 0x68; // Standard I2C Address for DS3231

I2C Configuration: Defines the I2C addresses of the connected devices.

  • LCD_ADDRESS: The I2C address of the LCD screen. 0x27 is common, but 0x3F or others are possible. You must ensure this matches your specific screen.
  • RTC_ADDRESS: The standard I2C address for the DS3231 RTC module (0x68).
// --- Object Initialization ---
RTC_DS3231 rtc;                  // Object for DS3231 RTC
LiquidCrystal_I2C lcd(LCD_ADDRESS, 20, 4); // Object for 20x4 LCD (address, columns, rows)

Object Initialization: Creates instances (objects) of the classes defined in the included libraries.

  • RTC_DS3231 rtc;: Creates an object named rtc that represents and allows control of the DS3231 clock.
  • LiquidCrystal_I2C lcd(...);: Creates an object named lcd to control the LCD screen. It's given the I2C address (LCD_ADDRESS) and the dimensions (20 columns, 4 rows).
// --- Pin Definitions --- (As per user's file arduinoIngles.TXT)
const int TRIG_PIN = 2;          // Ultrasonic sensor Trigger pin
const int ECHO_PIN = 3;          // Ultrasonic sensor Echo pin
const int MOTOR_IN1 = 7;         // Motor control pin 1
const int MOTOR_IN2 = 6;         // Motor control pin 2
const int OPEN_LIMIT_SWITCH = 8;   // Limit switch for fully open door
const int CLOSE_LIMIT_SWITCH = 9;  // Limit switch for fully closed door
const int HX711_DOUT_PIN = 4;    // HX711 Data Out pin (DT)
const int HX711_SCK_PIN = 5;     // HX711 Serial Clock pin (SCK)
// Button Pins
const int TARE_BUTTON_PIN = 12;    // Button to tare the scale
const int MODE_BUTTON_PIN = 13;    // Button to enter/cycle settings mode
const int UP_BUTTON_PIN = 11;      // Button to increment value in settings
const int DOWN_BUTTON_PIN = 10;    // Button to decrement value in settings

Pin Definitions: Assigns descriptive names to the Arduino pin numbers being used.

  • TRIG_PIN, ECHO_PIN: Pins for the ultrasonic sensor (Trigger sends the pulse, Echo receives the reflection).
  • MOTOR_IN1, MOTOR_IN2: Pins to control the motor's direction (likely connected to an H-Bridge driver).
  • OPEN_LIMIT_SWITCH, CLOSE_LIMIT_SWITCH: Pins for the limit switches that detect if the door is fully open or closed.
  • HX711_DOUT_PIN, HX711_SCK_PIN: Data Out (DT) and Serial Clock (SCK) pins for communication with the HX711 amplifier.
  • TARE_BUTTON_PIN, MODE_BUTTON_PIN, UP_BUTTON_PIN, DOWN_BUTTON_PIN: Pins for the Tare (zero the scale), Mode (enter/navigate settings), Up, and Down buttons.
// --- Configuration Constants ---
const unsigned long PRESENCE_TIMEOUT = 15000; // ms: Time door stays open after last presence
const int DISTANCE_THRESHOLD = 30;        // cm: Distance to detect presence/obstacle
const int SENSOR_READ_INTERVAL = 100;       // ms: How often to read sensors
const int MOTOR_START_DELAY = 50;         // ms: Small delay after starting motor
const int LCD_UPDATE_INTERVAL = 500;        // ms: How often to update LCD
const unsigned long DEBOUNCE_DELAY = 50;      // ms: Debounce time for buttons
const unsigned long MANUAL_OVERRIDE_HOLD_TIME = 1000; // ms: Hold time for manual override buttons
const unsigned long MOTOR_TIMEOUT_DURATION = 6000;  // <<< NEW: 6 seconds motor run timeout

Configuration Constants: Fixed values defining the system's behavior.

  • PRESENCE_TIMEOUT: Time (in milliseconds) the door stays open after presence is no longer detected (15 seconds).
  • DISTANCE_THRESHOLD: Distance (in centimeters) below which the ultrasonic sensor detects presence or an obstacle (30 cm).
  • SENSOR_READ_INTERVAL: How often (in milliseconds) the sensors (ultrasonic, limits, weight) are read (every 100 ms).
  • MOTOR_START_DELAY: A brief pause (in milliseconds) after starting the motor (50 ms). Used in manual override.
  • LCD_UPDATE_INTERVAL: How often (in milliseconds) the information on the LCD is refreshed (every 500 ms).
  • DEBOUNCE_DELAY: Time (in milliseconds) for button debouncing, preventing multiple triggers from electrical noise on a single press (50 ms).
  • MANUAL_OVERRIDE_HOLD_TIME: Time (in milliseconds) a button (Up/Down) must be held to activate manual override during restricted periods (1 second).
  • MOTOR_TIMEOUT_DURATION: Maximum time (in milliseconds) the motor can run continuously without hitting a limit switch before triggering an error state (6 seconds).
// --- Weight Unit Enum ---
enum WeightUnit { UNIT_GRAMS, UNIT_OUNCES };

// --- Settings Variables (loaded from EEPROM, with defaults) ---
int restrictedHourStart = 22; // Default restriction start time (10 PM)
int restrictedHourEnd = 6;   // Default restriction end time (6 AM)
WeightUnit currentUnit = UNIT_GRAMS; // Default weight unit

Enum and Settings Variables:

  • enum WeightUnit: Defines a custom data type WeightUnit that can only hold the values UNIT_GRAMS or UNIT_OUNCES. Used to represent the selected weight unit.
  • restrictedHourStart, restrictedHourEnd: Integer variables to store the start and end hour of the restricted period. They have default values (22:00 and 06:00) but will be loaded from EEPROM if valid data exists.
  • currentUnit: Variable of type WeightUnit storing the current weight unit. Defaults to UNIT_GRAMS. Also loaded/saved to EEPROM.
// --- HX711 Calibration ---
// !!! REPLACE THIS VALUE WITH YOUR OWN OBTAINED FROM CALIBRATION (using grams) !!!
const float CALIBRATION_FACTOR = 419.0; // Example value - MUST BE CHANGED!

HX711 Calibration:

  • CALIBRATION_FACTOR: CRITICAL! This floating-point value converts the raw reading from the HX711 into grams. You MUST determine this value for your specific load cell by placing a known weight on it and adjusting the factor until the reading is correct. The value 419.0 is just an example.
// --- EEPROM Addresses & Check Value ---
#define EEPROM_ADDR_R_START 0
#define EEPROM_ADDR_R_END   (EEPROM_ADDR_R_START + sizeof(int))
#define EEPROM_ADDR_UNIT    (EEPROM_ADDR_R_END + sizeof(int))
#define EEPROM_ADDR_CHECK   (EEPROM_ADDR_UNIT + sizeof(WeightUnit))
#define EEPROM_CHECK_VALUE  0x5B // Use a consistent check value

EEPROM Addresses & Check Value: Defines locations within the EEPROM memory where data will be stored.

  • EEPROM_ADDR_R_START: Address to store restrictedHourStart (starting at 0).
  • EEPROM_ADDR_R_END: Address for restrictedHourEnd (calculated by adding the size of an int to the previous address).
  • EEPROM_ADDR_UNIT: Address for currentUnit (calculated by adding the size of an int to the previous).
  • EEPROM_ADDR_CHECK: Address to store a check value (calculated by adding the size of WeightUnit to the previous).
  • EEPROM_CHECK_VALUE: A "magic" value (0x5B) stored at EEPROM_ADDR_CHECK. On startup, this value is read; if it matches, the data saved in EEPROM is considered valid. If not, default values are used, and everything (including the check value) is saved.
// --- HX711 Object Initialization ---
HX711_ADC LoadCell(HX711_DOUT_PIN, HX711_SCK_PIN);

HX711 Object Initialization:

  • HX711_ADC LoadCell(...): Creates an object LoadCell to interact with the HX711, telling it which Data (DT) and Clock (SCK) pins are connected.
// --- System States (Door) ---
enum State {
  INIT_OPENING, INIT_CLOSING, CLOSED, OPENING, OPEN, CLOSING, OBSTACLE_DETECTED,
  ERROR_MOTOR_TIMEOUT // Error State
};
State currentState = CLOSED;

System States:

  • enum State: Defines a custom data type State to represent the different operational states of the door (INIT_OPENING: Initializing opening, INIT_CLOSING: Initializing closing, CLOSED: Door is closed, OPENING: Door is currently opening, OPEN: Door is fully open, CLOSING: Door is currently closing, OBSTACLE_DETECTED: Obstacle found while closing (re-opening), ERROR_MOTOR_TIMEOUT: Error due to motor running too long).
  • State currentState = CLOSED;: Declares a variable currentState of type State to hold the system's current state. It's initialized to CLOSED.
// --- Global Variables ---
unsigned long lastPresenceTime = 0;
unsigned long lastSensorReadTime = 0;
unsigned long lastLcdUpdateTime = 0;
long currentDistance = -1;
bool openLimitReached = false;
bool closeLimitReached = false;
DateTime now;
float currentWeight = 0.0; // Stores weight in GRAMS
bool hx711_ready = false;
// Settings Mode
bool isInSettingsMode = false;
enum SettingField { NONE, SET_HOUR, SET_MINUTE, SET_RESTRICT_START, SET_RESTRICT_END, SET_UNIT }; // Uses only fields from arduinoIngles.TXT
SettingField currentSettingField = NONE;
int tempHour = 0; int tempMinute = 0; int tempR_Start = 0; int tempR_End = 0; WeightUnit tempUnit = UNIT_GRAMS;
// Button Debounce
bool lastTareState = HIGH; unsigned long lastTareDebounceTime = 0;
// ... (similar variables for Mode, Up, Down buttons)
bool stableTareState = HIGH; bool stableModeState = HIGH; bool stableUpState = HIGH; bool stableDownState = HIGH;
bool tarePressed = false; bool modePressed = false; bool upPressed = false; bool downPressed = false;
// Long Press
unsigned long upButtonPressStartTime = 0;
unsigned long downButtonPressStartTime = 0;
// Motor Timeout
unsigned long motorStartTime = 0; // <<< NEW: Timestamp when motor starts

Global Variables: Variables accessible from anywhere in the code.

  • lastPresenceTime: Stores the millis() timestamp of the last time presence was detected. Used for the PRESENCE_TIMEOUT.
  • lastSensorReadTime: Stores the timestamp of the last sensor reading cycle. Used for periodic reads (SENSOR_READ_INTERVAL).
  • lastLcdUpdateTime: Stores the timestamp of the last LCD update. Used for periodic updates (LCD_UPDATE_INTERVAL).
  • currentDistance: Stores the last distance measured by the ultrasonic sensor (-1 if invalid).
  • openLimitReached, closeLimitReached: Boolean flags indicating if the open or close limit switch is currently active (pressed).
  • DateTime now: An object that will hold the current date and time read from the RTC.
  • currentWeight: Stores the current measured weight (always in GRAMS internally).
  • hx711_ready: Boolean flag indicating if the HX711 was successfully initialized and is ready to provide readings.
  • isInSettingsMode: Boolean flag indicating if the system is currently in the configuration menu.
  • enum SettingField: Defines the different fields that can be edited in settings mode (NONE, SET_HOUR, SET_MINUTE, SET_RESTRICT_START, SET_RESTRICT_END, SET_UNIT).
  • currentSettingField: Variable indicating which specific field is currently selected for editing in settings mode.
  • tempHour, tempMinute, tempR_Start, tempR_End, tempUnit: Temporary variables to hold values while they are being modified in settings mode, before being saved permanently.
  • Debounce Variables (last...State, last...DebounceTime, stable...State): Used by the readButtons function to implement debouncing for each button. They store the last raw state read, the timestamp of the last detected change, and the stable state after the debounce delay.
  • Press Flags (...Pressed): Boolean flags set to true for one loop cycle when a debounced button press is detected.
  • Long Press Variables (...ButtonPressStartTime): Store the millis() timestamp when the UP or DOWN button was pressed. Used to detect if they are held down for MANUAL_OVERRIDE_HOLD_TIME.
  • motorStartTime: Stores the millis() timestamp when the motor started moving. Used for the MOTOR_TIMEOUT_DURATION. Set to 0 when the motor stop.
// --- Initial Setup ---
void setup() {
  Serial.begin(9600);
  Serial.println(F("Starting System V5.3 (Motor Timeout Added)...")); // Version update

  Wire.begin();

  // Init LCD
  lcd.init(); lcd.backlight(); lcd.setCursor(0, 0); lcd.print(F("Initializing Sys..."));
  Serial.println(F("LCD initialized."));

  // Init RTC
  if (!rtc.begin()) { /* Error handling */ }
  Serial.println(F("DS3231 RTC found."));
  if (rtc.lostPower()) { /* Handle power loss */ }
  // ... (RTC messages)

  // Init HX711
  Serial.println(F("Initializing HX711...")); // ... (HX711 messages)
  LoadCell.begin(); LoadCell.start(2000, true); // Start HX711, tare on startup
  if (LoadCell.getTareTimeoutFlag()) { /* Handle tare timeout */ }
  else { LoadCell.setCalFactor(CALIBRATION_FACTOR); /* Set factor */ hx711_ready = true; }
  // ... (HX711 messages)

  // Configure Pins
  pinMode(TRIG_PIN, OUTPUT); pinMode(ECHO_PIN, INPUT); // ... (set all pin modes)
  pinMode(OPEN_LIMIT_SWITCH, INPUT_PULLUP); // ... (using internal pull-ups for switches/buttons)

  // Initialize button states
  stableTareState=digitalRead(TARE_BUTTON_PIN); // ... (read initial stable states)
  lastTareState=stableTareState; // ... (sync last raw states)

  // Load config from EEPROM
  loadConfigFromEEPROM();

  stopMotor(); // Ensure motor is stopped initially
  readLimitSwitches(); // Read initial limit switch states

  // Determine initial door state based on limit switches
  if (closeLimitReached) { currentState = CLOSED; }
  else if (openLimitReached) { currentState = OPEN; }
  else { currentState = INIT_OPENING; openDoor(); /* Start opening */ }

  updateLcdDisplay(); // Show initial status on LCD
} // End setup

setup() Function: This function runs once when the Arduino powers up or resets.

  • Serial.begin(9600): Starts serial communication at 9600 baud for debugging messages.
  • Prints a startup message to the Serial Monitor.
  • Wire.begin(): Initializes the I2C communication bus.
  • Initializes the LCD (lcd.init(), turns on backlight lcd.backlight(), shows an init message).
  • Initializes the RTC (rtc.begin()). Includes error checking (if (!rtc.begin())) and checks if the RTC lost power (rtc.lostPower()) since the last run (useful for prompting time setting).
  • Initializes the HX711 (LoadCell.begin(), LoadCell.start()). The start(2000, true) attempts an initial tare with a 2-second timeout. It checks for tare timeout, sets the crucial CALIBRATION_FACTOR, and sets the hx711_ready flag if successful.
  • Configures Pin Modes: Sets each pin defined earlier to be either an OUTPUT or an INPUT. INPUT_PULLUP is used for buttons and limit switches, enabling the internal pull-up resistor (meaning the pin reads HIGH unless connected to ground, typically when the switch/button is pressed).
  • Initializes Button States: Reads the initial state of each button to correctly set the initial stable...State and last...State variables for the debounce logic.
  • loadConfigFromEEPROM(): Calls the function to load saved settings (restricted hours, unit) from EEPROM.
  • stopMotor(): Ensures the motor is initially off.
  • readLimitSwitches(): Reads the status of the limit switches at startup.
  • Determines Initial State: Checks the limit switches. If the close switch is active, state is CLOSED. If the open switch is active, state is OPEN. If neither is active (door is partially open), it enters the INIT_OPENING state and starts the openDoor() sequence to try and reach a known position (it will then transition to INIT_CLOSING).
  • updateLcdDisplay(): Updates the LCD with the initial status.
// --- Main Loop ---
void loop() {
  now = rtc.now(); // Get current time
  readButtons();   // Read button states (handles debounce)

  if (isInSettingsMode) {
    handleSettingsMode(); // Handle settings menu logic
  } else if (currentState == ERROR_MOTOR_TIMEOUT) {
    // In error state, do nothing actively here (display handled below)
    ; // Placeholder
  } else {
    // --- Normal Operation Mode ---
    unsigned long currentMillis = millis();
    if (tarePressed) { tareScale(); } // If Tare button pressed, tare the scale

    // Handle Manual Override (check conditions: restricted time, door state, button hold)
    bool override_action_taken = false;
    if (isRestrictedTimeNow() && currentState == CLOSED && stableUpState == LOW /* ... and hold time check */) {
       // ... Open door manually ...
       override_action_taken = true;
    } else if (isRestrictedTimeNow() && currentState == OPEN && stableDownState == LOW /* ... and hold time check */) {
       // ... Close door manually ...
       override_action_taken = true;
    }

    // Read Sensors periodically (if no override action just happened)
    if (!override_action_taken) {
      if (currentMillis - lastSensorReadTime >= SENSOR_READ_INTERVAL) {
        currentDistance = getDistance();
        readLimitSwitches();
        readWeight();
        lastSensorReadTime = currentMillis;
      }
    } else { lastSensorReadTime = currentMillis; } // Reset timer if override occurred

    // Handle State Machine (core door logic)
    handleStateMachine();

    // Check for entering settings mode
    if (modePressed) {
      isInSettingsMode = true; currentSettingField = SET_HOUR; // Enter settings
      // ... Initialize temp variables ...
      stopMotor(); // Stop motor when entering settings
      updateLcdDisplay(); // Update display immediately
    }
  } // End Normal Mode

  // Update LCD periodically
  if (millis() - lastLcdUpdateTime >= LCD_UPDATE_INTERVAL) {
    updateLcdDisplay();
    lastLcdUpdateTime = millis();
  }
} // End loop

loop() Function: This function runs repeatedly after setup() completes.

  • now = rtc.now(): Gets the current date and time from the RTC object.
  • readButtons(): Calls the function to read all buttons, handle debouncing, and set the ...Pressed flags.
  • Conditional Logic:
    • If isInSettingsMode is true, it calls handleSettingsMode() to manage the settings menu.
    • Else if currentState is ERROR_MOTOR_TIMEOUT, it currently does nothing active within the loop (the LCD update handles displaying the error).
    • Else (Normal Operation Mode):
      • Gets the current millis() time.
      • Checks if the tarePressed flag is set; if so, calls tareScale().
      • Checks for Manual Override conditions: Is it restricted time? Is the door closed/open? Is the appropriate button (UP/DOWN) being held down (stable...State == LOW) for longer than MANUAL_OVERRIDE_HOLD_TIME? If conditions met, initiates manual open/close, sets override_action_taken flag.
      • Reads Sensors: If no manual override action just occurred, it checks if SENSOR_READ_INTERVAL has passed since the last read. If yes, it calls getDistance(), readLimitSwitches(), readWeight(), and updates lastSensorReadTime.
      • handleStateMachine(): Calls the core function that manages door state transitions based on sensor inputs and time.
      • Checks if the modePressed flag is set. If yes, it enters Settings Mode (isInSettingsMode = true), sets the initial field to edit, copies current settings to temporary variables, stops the motor, and updates the LCD immediately.
  • Periodically updates the LCD: Checks if LCD_UPDATE_INTERVAL has passed since the last update. If yes, calls updateLcdDisplay() and updates lastLcdUpdateTime.
// Reads buttons, handles debounce, sets flags, tracks long press start times
void readButtons() {
  tarePressed = false; modePressed = false; upPressed = false; downPressed = false; // Reset flags
  unsigned long now_ms = millis();

  // --- TARE --- (Example for one button, others are similar)
  bool rT = digitalRead(TARE_BUTTON_PIN); // Read raw pin state
  if (rT != lastTareState) { lastTareDebounceTime = now_ms; } // If state changed, reset debounce timer
  if ((now_ms - lastTareDebounceTime) > DEBOUNCE_DELAY) { // If debounce delay passed
    if (rT != stableTareState) { // If stable state needs update
      stableTareState = rT; // Update stable state
      if (stableTareState == LOW) { // If newly pressed (stable LOW)
        tarePressed = true; // Set the flag for this loop cycle
        Serial.println(F(">>> TARE Pressed <<<"));
      }
    }
  }
  lastTareState = rT; // Store current raw state for next comparison

  // --- MODE --- (Similar logic)
  // --- UP --- (Similar logic, plus long press start time)
  // ...
     if (stableUpState == LOW) { upPressed = true; /*...*/ upButtonPressStartTime = now_ms; } // Record press start time
     else { upButtonPressStartTime = 0; } // Reset start time on release
  // ...
  // --- DOWN --- (Similar logic, plus long press start time)
  // ...
}

readButtons() Function: Reads all buttons, applies debouncing, and sets flags.

  • Resets all ...Pressed flags to false at the beginning.
  • Gets the current time (millis()).
  • For each button (Tare, Mode, Up, Down):
    • Reads the current digital state of the pin (digitalRead). Remember INPUT_PULLUP means LOW is pressed.
    • Compares the current state (rT, rM, etc.) to the last recorded raw state (lastTareState, etc.). If different, it means a change occurred (or noise), so the last...DebounceTime is updated to the current time.
    • Checks if the time elapsed since last...DebounceTime is greater than DEBOUNCE_DELAY. Only proceeds if enough time has passed without changes, ensuring the signal is stable.
    • If debounced, it checks if the current raw state (rT) is different from the currently accepted stableTareState.
    • If it's different, the stableTareState is updated to the new stable value (rT).
    • If the new stableTareState is LOW (meaning a stable press was just confirmed), the corresponding ...Pressed flag (tarePressed) is set to true for this one iteration of the loop(). A debug message is printed.
    • For UP and DOWN buttons: If the stable state becomes LOW, it also records the current time in upButtonPressStartTime or downButtonPressStartTime. If the stable state becomes HIGH (released), the corresponding start time is reset to 0.
    • Finally, the lastTareState (raw state) is updated for the next comparison.
// Performs tare operation
void tareScale() {
   if (!hx711_ready) { /*...*/ return; } // Check HX711 status
   Serial.println(F("Taring scale..."));
   lcd.setCursor(0, 0); lcd.print(F("Taring...        "));
   LoadCell.tare(); // Perform tare using the library function
   currentWeight = 0.0; // Reset internal weight variable
   delay(100); // Small delay
   updateLcdDisplay(); // Update LCD immediately
   Serial.println(F("Scale tared."));
}

tareScale() Function: Executes the tare (zeroing) operation for the load cell.

  • Checks if the hx711_ready flag is true. If not, prints an error and returns.
  • Prints status messages to Serial and LCD.
  • Calls LoadCell.tare() which commands the HX711 library to perform the tare operation (find the current zero offset).
  • Resets the currentWeight variable to 0.0.
  • Includes a small delay(100).
  • Calls updateLcdDisplay() to show the result immediately.
  • Prints a confirmation message to Serial.
// Handles settings menu navigation and value changes
void handleSettingsMode() {
  // MODE Button: Cycle or Save/Exit
  if (modePressed) {
    int currentIdx = (int)currentSettingField; // Get current field index
    int nextIdx = currentIdx + 1; // Calculate next index
    if (nextIdx > SET_UNIT) { // If currently on the last item (SET_UNIT)
      // --- Save and Exit ---
      Serial.println(F("Saving configuration and exiting..."));
      now = rtc.now(); // Get current time for setting RTC
      // Update RTC time with tempHour/tempMinute
      rtc.adjust(DateTime(now.year(), now.month(), now.day(), tempHour, tempMinute, now.second()));
      // Update global settings variables from temp variables
      restrictedHourStart = tempR_Start; restrictedHourEnd = tempR_End;
      currentUnit = tempUnit;
      saveConfigToEEPROM(); // Save relevant settings to EEPROM
      currentSettingField = NONE; // Reset current field
      isInSettingsMode = false; // Exit settings mode
      Serial.println(F("Exited Settings Mode."));
      return; // Exit function immediately
    } else {
      // Move to next field
      currentSettingField = (SettingField)nextIdx; // Cast next index back to enum type
      Serial.print(F("New field selected: ")); Serial.println(getSettingFieldString(currentSettingField));
      updateLcdDisplay(); // Update LCD immediately
    }
  } // End if modePressed

  // UP/DOWN Buttons: Modify temporary value
  if (upPressed || downPressed) {
    int delta = upPressed ? 1 : -1; // Determine change direction (+1 or -1)
    bool valueChanged = true;
    switch (currentSettingField) { // Check which field is being edited
      case SET_HOUR: tempHour = (tempHour + delta + 24) % 24; break; // Adjust hour (0-23 wrap)
      case SET_MINUTE: tempMinute = (tempMinute + delta + 60) % 60; break; // Adjust minute (0-59 wrap)
      case SET_RESTRICT_START: tempR_Start = (tempR_Start + delta + 24) % 24; break; // Adjust start hour
      case SET_RESTRICT_END: tempR_End = (tempR_End + delta + 24) % 24; break; // Adjust end hour
      case SET_UNIT: tempUnit = (tempUnit == UNIT_GRAMS) ? UNIT_OUNCES : UNIT_GRAMS; break; // Toggle unit
      default: valueChanged = false; break; // No action if field is NONE
    }
    if (valueChanged) {
      Serial.print(F(" Temp value updated for: ")); Serial.println(getSettingFieldString(currentSettingField));
      updateLcdDisplay(); // Update LCD immediately if value changed
    }
  }
}

handleSettingsMode() Function: Manages user interaction within the settings menu.

  • Handles MODE Button Press:
    • Gets the numerical index of the currentSettingField.
    • Calculates the index of the next field.
    • If the next index is beyond the last field (SET_UNIT), it means the user pressed MODE on the last item, so it triggers the "Save and Exit" sequence:
      • Gets the current date/time from RTC.
      • Adjusts the RTC time using the temporary tempHour and tempMinute (rtc.adjust).
      • Copies the temporary settings (tempR_Start, tempR_End, tempUnit) into the main global variables (restrictedHourStart, etc.).
      • Calls saveConfigToEEPROM() to persist the restriction times and unit setting.
      • Resets currentSettingField to NONE.
      • Sets isInSettingsMode to false.
      • Returns immediately from the function.
    • Otherwise (not the last field), it updates currentSettingField to the next field in the sequence and updates the LCD immediately.
  • Handles UP/DOWN Button Press:
    • Determines the change amount (delta = 1 for UP, -1 for DOWN).
    • Uses a switch statement based on the currentSettingField.
    • Adjusts the corresponding temporary variable (tempHour, tempMinute, etc.). The modulo operator (%) is used with the range (e.g., + 24) % 24) to ensure values wrap around correctly (e.g., 23 + 1 becomes 0, 0 - 1 becomes 23).
    • For SET_UNIT, it simply toggles the tempUnit between UNIT_GRAMS and UNIT_OUNCES.
    • If a value was actually changed, it updates the LCD immediately.
// Updates the LCD display based on current mode (Normal or Settings)
void updateLcdDisplay() {
  char lineBuffer[21], floatBuffer[10]; // Buffers for formatting strings
  const float G_TO_OZ_FACTOR = 0.035274; // Conversion factor

  // *** Handle ERROR_MOTOR_TIMEOUT state display ***
  if (currentState == ERROR_MOTOR_TIMEOUT) {
    lcd.clear();
    lcd.setCursor(0, 0); lcd.print(F("!!! MOTOR ERROR !!! "));
    lcd.setCursor(0, 1); lcd.print(F(" Movement Timeout   "));
    lcd.setCursor(0, 2); lcd.print(F(" Check Door / Limit "));
    lcd.setCursor(0, 3); lcd.print(F(" Switches!          "));
    return; // Stop further display updates in error state
  }

  lcd.clear(); // Clear display for normal/settings redraw

  if (isInSettingsMode) {
    // --- Settings Mode Interface ---
    lcd.setCursor(0, 0); lcd.print(F("SETUP:")); lcd.print(getSettingFieldString(currentSettingField)); // Line 0: Title + Field

    // Line 1: Time (temp values) + indicator '<' if editing Hour/Minute
    lcd.setCursor(0, 1); snprintf(lineBuffer, sizeof(lineBuffer), "Time:   %02d:%02d %c", tempHour, tempMinute, (currentSettingField == SET_HOUR || currentSettingField == SET_MINUTE) ? '<' : ' '); lcd.print(lineBuffer);
    // Line 2: Restriction Times (temp values) + indicator '<' if editing Start/End
    lcd.setCursor(0, 2); snprintf(lineBuffer, sizeof(lineBuffer), "Restr:  %02d - %02d %c", tempR_Start, tempR_End, (currentSettingField == SET_RESTRICT_START || currentSettingField == SET_RESTRICT_END) ? '<' : ' '); lcd.print(lineBuffer);
    // Line 3: Unit (temp value) + indicator '<' if editing Unit
    lcd.setCursor(0, 3); snprintf(lineBuffer, sizeof(lineBuffer), "Unit: %-7s %c", getUnitString(tempUnit), (currentSettingField == SET_UNIT) ? '<' : ' '); lcd.print(lineBuffer);

  } else {
    // --- Normal Operation Interface ---
    // Line 0: Weight
    lcd.setCursor(0, 0);
    if (hx711_ready) {
        float dW=currentWeight; const char* uS=" g"; int p=1; // Default grams, 1 decimal place
        if(currentUnit==UNIT_OUNCES){dW*=G_TO_OZ_FACTOR; uS=" oz"; p=2;} // If ounces, convert & change suffix/precision
        dtostrf(dW,1,p,floatBuffer); // Convert float to string
        snprintf(lineBuffer,sizeof(lineBuffer),"Weight:%-7s%s",floatBuffer,uS); // Format line
    } else { snprintf(lineBuffer, sizeof(lineBuffer), "Weight: ---.-      "); } // Show placeholder if not ready
    lcd.print(lineBuffer); // Print weight line
    // ... (Pad rest of line with spaces - this happens for all lines) ...

    // Line 1: Time (current time from 'now')
    snprintf(lineBuffer,sizeof(lineBuffer),"Time: %02d:%02d:%02d",now.hour(),now.minute(),now.second()); lcd.setCursor(0,1); lcd.print(lineBuffer); //...pad...

    // Line 2: Status (current door state)
    snprintf(lineBuffer, sizeof(lineBuffer), "Status: %-12s", getStateString(currentState)); lcd.setCursor(0, 2); lcd.print(lineBuffer); //...pad...

    // Line 3: Distance or Restriction Status
    lcd.setCursor(0,3);
    if(isRestrictedTimeNow()){ // If restricted time
      snprintf(lineBuffer,sizeof(lineBuffer),"Restr: %02d-%02dh ACTIVE", restrictedHourStart, restrictedHourEnd);
    } else { // If not restricted time, show distance
      if(currentDistance>=0 && currentDistance<999){ // If valid distance
        snprintf(lineBuffer,sizeof(lineBuffer),"Dist: %3ld cm      ",currentDistance);
      } else { // If invalid distance
        snprintf(lineBuffer,sizeof(lineBuffer),"Dist: --- cm      ");
      }
    }
    lcd.print(lineBuffer); //...pad...
  }
}

updateLcdDisplay() Function: Controls what is shown on the 20x4 LCD screen.

  • Declares character arrays (lineBuffer, floatBuffer) for formatting text. Defines the grams-to-ounces conversion factor.
  • Error State Handling: First, it checks if currentState is ERROR_MOTOR_TIMEOUT. If so, it clears the LCD and displays a specific 4-line error message about the motor timeout, then returns immediately, skipping the rest of the display logic.
  • Clears the LCD (lcd.clear()) before redrawing in normal or settings mode.
  • Settings Mode Display: If isInSettingsMode is true:
    • Line 0: Shows "SETUP:" followed by the name of the field currently being edited (using getSettingFieldString).
    • Line 1: Shows the temporary time (tempHour:tempMinute). Adds a '<' character at the end if SET_HOUR or SET_MINUTE is the currentSettingField.
    • Line 2: Shows the temporary restriction hours (tempR_Start - tempR_End). Adds '<' if editing start or end hour.
    • Line 3: Shows the temporary selected unit (using getUnitString). Adds '<' if editing the unit.
  • Normal Mode Display: If not in settings mode:
    • Line 0 (Weight): Checks if hx711_ready. If yes, it gets the currentWeight. If currentUnit is Ounces, it converts the weight using G_TO_OZ_FACTOR and sets the unit string to " oz" and precision to 2 decimal places; otherwise, it uses " g" and 1 decimal place. It uses dtostrf to convert the float weight to a string (floatBuffer) and snprintf to format the final line "Weight: VALUE UNIT". If HX711 is not ready, it displays "Weight: ---.- ". The line is padded with spaces to clear previous content.
    • Line 1 (Time): Formats the current time (now.hour(), minute(), second()) into "Time: HH:MM:SS" using snprintf. Padded.
    • Line 2 (Status): Formats "Status: " followed by the string representation of the currentState (using getStateString). Padded.
    • Line 3 (Distance/Restriction): Checks isRestrictedTimeNow(). If true, displays "Restr: HH-HHh ACTIVE". If false, it checks if currentDistance is valid (0-998). If valid, displays "Dist: VALUE cm". If invalid, displays "Dist: --- cm". Padded.
// Converts SettingField enum to a printable string
const char* getSettingFieldString(SettingField sf) {
  switch (sf) { /* ... cases returning string literals like "HOUR", "MINUTE", etc. ... */ }
}

// Converts WeightUnit enum to a printable string (English)
const char* getUnitString(WeightUnit u) {
  return (u == UNIT_GRAMS) ? "GRAMS" : "OUNCES";
}

 

getSettingFieldString() & getUnitString() Functions: Helper functions to convert enum values into human-readable strings for display.

  • getSettingFieldString: Takes a SettingField value and returns the corresponding string (e.g., "HOUR", "R.START").
  • getUnitString: Takes a WeightUnit value and returns either "GRAMS" or "OUNCES".
// Reads weight from HX711
void readWeight() { if (hx711_ready) { if (LoadCell.update()) { currentWeight = LoadCell.getData(); } } }

// Reads limit switches
void readLimitSwitches() { openLimitReached=(digitalRead(OPEN_LIMIT_SWITCH)==LOW); closeLimitReached=(digitalRead(CLOSE_LIMIT_SWITCH)==LOW); }

readWeight() & readLimitSwitches() Functions: Read data from specific sensors.

  • readWeight: Checks if hx711_ready. If yes, calls LoadCell.update() (which checks if new data is available). If data is available, it updates the global currentWeight by calling LoadCell.getData().
  • readLimitSwitches: Reads the digital state of the OPEN_LIMIT_SWITCH and CLOSE_LIMIT_SWITCH pins. Since INPUT_PULLUP is used, the switch is active (pressed) when the pin reads LOW. The boolean flags openLimitReached and closeLimitReached are updated accordingly.
// Checks if current time is within restricted period
bool isRestrictedTimeNow() {
  int cH=now.hour(); // Get current hour
  bool r=false;
  if(restrictedHourStart > restrictedHourEnd){ // Case 1: Restriction crosses midnight (e.g., 22:00 to 06:00)
    r=(cH>=restrictedHourStart || cH<restrictedHourEnd); // Restricted if current hour >= start OR < end
  } else { // Case 2: Restriction within the same day (or start == end)
    if(restrictedHourStart == restrictedHourEnd) return false; // If start == end, no restriction
    r=(cH>=restrictedHourStart && cH<restrictedHourEnd); // Restricted if current hour >= start AND < end
  }
  return r;
}

sRestrictedTimeNow() Function: Determines if the current time falls within the configured restricted period.

  • Gets the current hour (cH) from the global now object.
  • Handles the case where the restriction period crosses midnight (e.g., start = 22, end = 6). In this case, it's restricted if the current hour is greater than or equal to the start hour OR less than the end hour.
  • Handles the case where start and end hours are the same (no restriction).
  • Handles the case where the restriction is within a single day (e.g., start = 9, end = 17). It's restricted if the current hour is greater than or equal to the start hour AND less than the end hour.
  • Returns true if the current time is restricted, false otherwise.
// Main state machine for door operation
void handleStateMachine() {
  // Don't run state machine if in settings or error state
  if (isInSettingsMode || currentState == ERROR_MOTOR_TIMEOUT) return;

  State previousState = currentState; // Store state before processing

  // --- Motor Timeout Check ---
  // Check only if motor is supposed to be moving and timer is running
  if (motorStartTime != 0 && (currentState == OPENING || currentState == CLOSING || currentState == INIT_OPENING || currentState == INIT_CLOSING)) {
    if (millis() - motorStartTime > MOTOR_TIMEOUT_DURATION) { // If timeout exceeded
      Serial.println(F("!!! MOTOR TIMEOUT !!! State was: ")); Serial.println(getStateString(currentState));
      stopMotor(); // Stop motor (also resets motorStartTime)
      currentState = ERROR_MOTOR_TIMEOUT; // Enter error state
      updateLcdDisplay(); // Update LCD immediately to show error
      lastLcdUpdateTime = millis();
      return; // Exit state machine handling for this cycle
    }
  }

  // --- Normal State Transitions ---
  switch (currentState) {
    case INIT_OPENING: // Initializing: trying to open fully first
      if (openLimitReached) { /*...*/ stopMotor(); currentState=INIT_CLOSING; closeDoor(); }
      break;
    case INIT_CLOSING: // Initializing: now trying to close fully
      if (closeLimitReached) { /*...*/ stopMotor(); currentState=CLOSED; }
      break;
    case CLOSED: // Door is closed
      if (currentDistance>=0 && currentDistance<DISTANCE_THRESHOLD) { // If presence detected
        if (!isRestrictedTimeNow()) { // And not restricted time
          /*...*/ currentState=OPENING; openDoor(); // Start opening
        } else { /* Restricted: do nothing (maybe log?) */ }
      }
      break;
    case OPENING: // Door is opening
      if (openLimitReached) { /*...*/ stopMotor(); currentState=OPEN; } // Stop when open limit hit
      break;
    case OPEN: // Door is open
      if (currentDistance>=0 && currentDistance<DISTANCE_THRESHOLD) { lastPresenceTime=millis(); } // Update last presence time if detected
      if (millis()-lastPresenceTime >= PRESENCE_TIMEOUT) { // If timeout passed since last presence
        /*...*/ currentState=CLOSING; closeDoor(); // Start closing
      }
      break;
    case CLOSING: // Door is closing
      if (currentDistance>=0 && currentDistance<DISTANCE_THRESHOLD) { // If obstacle detected
        /*...*/ stopMotor(); currentState=OBSTACLE_DETECTED; openDoor(); // Stop, change state, start re-opening
      } else if (closeLimitReached) { // If close limit hit
        /*...*/ stopMotor(); currentState=CLOSED; // Stop, change state to closed
      }
      break;
    case OBSTACLE_DETECTED: // Door is re-opening after obstacle
      if (openLimitReached) { // If fully re-opened
        /*...*/ stopMotor(); currentState=OPEN; lastPresenceTime=millis(); // Stop, change state to open, reset presence timer
      }
      break;
    // ERROR_MOTOR_TIMEOUT case intentionally empty here - handled at start
  }

  // If the state changed (and it wasn't to the error state already handled), update LCD
  if (currentState != previousState) {
     Serial.print(F("State Change: ")); Serial.println(getStateString(currentState));
     updateLcdDisplay();
     lastLcdUpdateTime = millis();
  }
}

handleStateMachine() Function: The core logic that determines the door's actions based on its current state and inputs.

  • Returns immediately if the system is in Settings Mode or already in the ERROR_MOTOR_TIMEOUT state.
  • Stores the current state in previousState to detect changes later.
  • Motor Timeout Check:
    • Checks if the motor timer is running (motorStartTime != 0) AND if the currentState is one where the motor should be moving (OPENING, CLOSING, INIT_...).
    • If both are true, it checks if the elapsed time (millis() - motorStartTime) has exceeded MOTOR_TIMEOUT_DURATION.
    • If the timeout is exceeded: prints error messages, calls stopMotor() (which also resets motorStartTime), sets currentState to ERROR_MOTOR_TIMEOUT, updates the LCD immediately to show the error, and returns, preventing further state logic in this cycle.
  • Normal State Transitions: Uses a switch statement based on the currentState:
    • INIT_OPENING: If the open limit switch is reached, stop the motor, change state to INIT_CLOSING, and start closing (closeDoor()).
    • INIT_CLOSING: If the close limit switch is reached, stop the motor and change state to CLOSED. The system is now initialized.
    • CLOSED: If presence is detected (currentDistance < threshold) AND it's not restricted time (!isRestrictedTimeNow()), change state to OPENING and start opening (openDoor()).
    • OPENING: If the open limit switch is reached, stop the motor and change state to OPEN.
    • OPEN: If presence is detected, update lastPresenceTime to the current time. If the time elapsed since lastPresenceTime exceeds PRESENCE_TIMEOUT, change state to CLOSING and start closing (closeDoor()).
    • CLOSING: If presence (an obstacle) is detected, stop the motor, change state to OBSTACLE_DETECTED, and start opening again (openDoor()). If no obstacle and the close limit switch is reached, stop the motor and change state to CLOSED.
    • OBSTACLE_DETECTED (Door is re-opening): If the open limit switch is reached, stop the motor, change state back to OPEN, and reset lastPresenceTime (treating the obstacle detection like a new presence event).
    • ERROR_MOTOR_TIMEOUT: No transitions are defined from this state within the switch (it's handled at the beginning and in the main loop).
  • State Change Update: After the switch statement, if the currentState is different from previousState, it means a state transition occurred. It prints the new state to Serial and calls updateLcdDisplay() to show the change immediately.
// Converts State enum to a printable string (English, No F() in return)
// Added ERROR_MOTOR_TIMEOUT case
const char* getStateString(State s) {
 switch(s){
     case INIT_OPENING: return "Init Opening"; // ... other cases ...
     case ERROR_MOTOR_TIMEOUT: return "ERROR TIMEOUT";
     default: return "Unknown";
 }
}

getStateString() Function: Returns a user-friendly string corresponding to a given State enum value. Used for display on the LCD and Serial monitor. Includes the string for the new ERROR_MOTOR_TIMEOUT state.

// Measures distance using ultrasonic sensor
long getDistance() {
 digitalWrite(TRIG_PIN,LOW);delayMicroseconds(2); // Ensure TRIG is low
 digitalWrite(TRIG_PIN,HIGH);delayMicroseconds(10); // Send 10us HIGH pulse
 digitalWrite(TRIG_PIN,LOW); // Set TRIG low again
 // Measure time for ECHO pulse to return (timeout 25000us = 25ms)
 long d=pulseIn(ECHO_PIN,HIGH,25000);
 // Calculate distance: (duration * speed_of_sound / 2)
 // speed_of_sound = 0.0343 cm/us approx.
 return (d==0) ? 999 : d*0.0343/2.0; // Return 999 if timeout (d=0), else calculated distance
}

getDistance() Function: Measures distance using the ultrasonic sensor (HC-SR04 type logic).

  • Sends a 10-microsecond pulse out from the TRIG_PIN.
  • Uses pulseIn(ECHO_PIN, HIGH, 25000) to measure the time (in microseconds) it takes for the echo pulse to return to the ECHO_PIN. It includes a timeout of 25000 microseconds (25 ms).
  • If pulseIn times out, it returns 0. The function checks for this and returns 999 (an invalid distance value).
  • If a valid duration d is received, it calculates the distance in centimeters using the formula: distance = (duration * speed_of_sound) / 2. The speed of sound is approximately 0.0343 cm/microsecond. The division by 2 accounts for the sound traveling to the object and back.
// Motor control functions - Added motorStartTime handling
void openDoor() {
  digitalWrite(MOTOR_IN1,HIGH); digitalWrite(MOTOR_IN2,LOW); // Set pins for opening direction
  motorStartTime = millis(); // <<< Start timer
  Serial.println(F("Motor: Opening"));
}
void closeDoor() {
  digitalWrite(MOTOR_IN1,LOW); digitalWrite(MOTOR_IN2,HIGH); // Set pins for closing direction
  motorStartTime = millis(); // <<< Start timer
  Serial.println(F("Motor: Closing"));
}
void stopMotor() {
  digitalWrite(MOTOR_IN1,LOW); digitalWrite(MOTOR_IN2,LOW); // Set both pins LOW to stop (brake/coast depends on H-bridge)
  motorStartTime = 0; // <<< Stop timer
  Serial.println(F("Motor: Stopped"));
}

Motor Control Functions (openDoor, closeDoor, stopMotor): Simple functions to control the motor direction and state, and manage the motor timeout timer.

  • openDoor: Sets MOTOR_IN1 HIGH and MOTOR_IN2 LOW (assuming this combination drives the motor in the "open" direction). Records the start time in motorStartTime. Prints status.
  • closeDoor: Sets MOTOR_IN1 LOW and MOTOR_IN2 HIGH (assuming this drives the motor in the "close" direction). Records the start time in motorStartTime. Prints status.
  • stopMotor: Sets both MOTOR_IN1 and MOTOR_IN2 LOW (this usually stops the motor; behavior might be brake or coast depending on the H-bridge). Resets motorStartTime to 0, effectively stopping the timeout timer. Prints status.
// Loads configuration from EEPROM
void loadConfigFromEEPROM() {
  Serial.println(F("Loading config from EEPROM..."));
  byte checkValue = EEPROM.read(EEPROM_ADDR_CHECK); // Read the check value

  if (checkValue == EEPROM_CHECK_VALUE) { // If check value matches
    // Read saved values into global variables
    EEPROM.get(EEPROM_ADDR_R_START, restrictedHourStart);
    EEPROM.get(EEPROM_ADDR_R_END, restrictedHourEnd);
    EEPROM.get(EEPROM_ADDR_UNIT, currentUnit);
    // Sanity check for loaded unit
    if (currentUnit != UNIT_GRAMS && currentUnit != UNIT_OUNCES) { currentUnit = UNIT_GRAMS; /*...*/ }
    Serial.println(F("Configuration loaded."));
  } else { // If check value doesn't match (invalid/first run)
    Serial.println(F("EEPROM invalid. Saving defaults."));
    // Ensure global variables have default values (redundant but safe)
    restrictedHourStart = 22; restrictedHourEnd = 6; currentUnit = UNIT_GRAMS;
    saveConfigToEEPROM(); // Save these defaults to EEPROM
    EEPROM.write(EEPROM_ADDR_CHECK, EEPROM_CHECK_VALUE); // Write the correct check value now
    Serial.println(F("Default values saved."));
  }
  // Print loaded/default values for confirmation
  Serial.print(F("  Restr Start: ")); Serial.println(restrictedHourStart);
  // ... (print other loaded values) ...
}

loadConfigFromEEPROM() Function: Reads settings stored in EEPROM at startup.

  • Prints a status message.
  • Reads the single byte stored at EEPROM_ADDR_CHECK.
  • Compares the read checkValue with the predefined EEPROM_CHECK_VALUE.
  • If they match:
    • It means valid data was previously saved. It uses EEPROM.get() to read the values for restrictedHourStart, restrictedHourEnd, and currentUnit from their respective addresses directly into the global variables.
    • Includes a small sanity check to ensure the loaded currentUnit is valid.
    • Prints a confirmation message.
  • If the check values do not match:
    • It means the EEPROM data is invalid (or it's the first time running). It prints a message indicating defaults are being used.
    • It ensures the global variables hold the default values (though they should already from their definition).
    • It calls saveConfigToEEPROM() to write these default values into the EEPROM addresses.
    • Crucially, it then writes the correct EEPROM_CHECK_VALUE to EEPROM_ADDR_CHECK so that the data will be considered valid on the next boot.
    • Prints a confirmation message.
  • Finally, it prints the values (either loaded or default) that are now active in the global variables.
// Saves configuration to EEPROM
void saveConfigToEEPROM() {
  Serial.println(F("Saving config to EEPROM..."));
  // Write current global variable values to EEPROM addresses
  EEPROM.put(EEPROM_ADDR_R_START, restrictedHourStart);
  EEPROM.put(EEPROM_ADDR_R_END, restrictedHourEnd);
  EEPROM.put(EEPROM_ADDR_UNIT, currentUnit);
  Serial.println(F("Configuration saved."));
  // NOTE: This function doesn't update the check value; it assumes it was set correctly
  // during setup or doesn't need changing if only values are updated.
}
  1. saveConfigToEEPROM() Function: Writes the current settings from global variables into EEPROM.
    • Prints a status message.
    • Uses EEPROM.put() to write the current values of restrictedHourStart, restrictedHourEnd, and currentUnit to their designated addresses in EEPROM. EEPROM.put() handles different data types automatically.
    • Prints a confirmation message.
    • Note: This function specifically saves the configuration settings but does not rewrite the EEPROM_CHECK_VALUE. The check value is typically written only once when defaults are saved after detecting invalid data.

 

 

 

 

License:

Creative Commons — Attribution — Noncommercial — Share Alike

Related Models

Cable Corners... keep cables in corners! preview image

Cable Corners... keep cables in corners!

muzz64 profile image

muzz64

30,982

Headset Hanger 2.0 preview image

Headset Hanger 2.0

RMTB profile image

RMTB

16,768

Baby Birth Tag Holder for the wall  preview image

Baby Birth Tag Holder for the wall

Morganja profile image

Morganja

3,959

Universal Filament Filter and Lubricator preview image

Universal Filament Filter and Lubricator

CreativeTools profile image

CreativeTools

26,111

3D4U by Miele Coffee Clip preview image

3D4U by Miele Coffee Clip

3D4U powered by Miele profile image

3D4U powered by Miele

8,649

Haus-Familienaufsteller „Home“ mit Namensschildern preview image

Haus-Familienaufsteller „Home“ mit Namensschildern

Daniel Lippert profile image

Daniel Lippert

694

SHARKZ... Fun Multipurpose Clips / Holders / Pegs with moving jaws that bite! preview image

SHARKZ... Fun Multipurpose Clips / Holders / Pegs with moving jaws that bite!

muzz64 profile image

muzz64

24,677

Mechanical wall clock preview image

Mechanical wall clock

Harald Andersson profile image

Harald Andersson

6,972