From 474f0ec63a71ae73003fe36c5dd9aece66ec86d5 Mon Sep 17 00:00:00 2001 From: psi2100 Date: Mon, 10 Feb 2025 06:04:18 +0000 Subject: [PATCH] New files new files --- auto.h | 300 ++++++++++++++++++++++++++++++++++++++ bluetooth.h | 110 ++++++++++++++ commands.h | 410 ++++++++++++++++++++++++++++++++++++++++++++++++++++ commands.md | 58 ++++++++ config.h | 208 ++++++++++++++++++++++++++ 5 files changed, 1086 insertions(+) create mode 100644 auto.h create mode 100644 bluetooth.h create mode 100644 commands.h create mode 100644 commands.md create mode 100644 config.h diff --git a/auto.h b/auto.h new file mode 100644 index 0000000..b230e29 --- /dev/null +++ b/auto.h @@ -0,0 +1,300 @@ +/** + * @file auto.h + * @brief Auto mode control routines for the GreenHouse Controller. + * + * This module implements the automatic control logic for the greenhouse. + * It handles: + * - Managing the mixing tank fill pump based on tank levels. + * - Adjusting pH by activating the pH pump and mixer pump. + * - Controlling plant watering by comparing soil moisture readings + * against per-plant setpoints. + * + * The auto mode functions use one-shot logging to reduce Serial output spam. + * Additionally, they ensure that plant watering is not performed unless the mixer + * tank is full, pH balanced, and has sufficient fluid. + * + * Per-plant configuration is stored in the global array 'plantConfigs', which is defined in config.h. + * + * @author + * James C. Alexander + * @date + * 2025-02-03 + */ + +#ifndef AUTO_H +#define AUTO_H + +#include "config.h" +#include "pumps.h" +#include "sensors.h" +#include "ultrasonic.h" + +// --------------------------------------------------------------------- +// File-Scope Global State Variables for Auto Mode +// --------------------------------------------------------------------- +// These static variables persist across multiple function calls within this module. + +/** + * @brief Flag used to log the entry into auto mode only once per cycle. + */ +static bool autoModeLogged = false; + +/** + * @brief Flag indicating whether the mixer tank pH has been balanced in the current fill cycle. + */ +static bool phBalanced = false; + +/** + * @brief One-shot logging arrays for per-plant moisture thresholds. + * + * These arrays store the last logged low and high thresholds for each plant. + */ +static int lastLow[3] = { -1, -1, -1 }; +static int lastHigh[3] = { -1, -1, -1 }; + +/** + * @brief Stores the last state of the fill pump. + * + * (Active-low: LOW means pump is ON; HIGH means pump is OFF.) + */ +static bool lastMixPumpState = HIGH; + +/** + * @brief Stores the last known state of each plant's pump. + * + * The size of the array is determined by the number of moisture sensor pins. + */ +static bool lastPlantPumpState[sizeof(MOISTURE_PINS) / sizeof(MOISTURE_PINS[0])] = { HIGH, HIGH, HIGH }; + +/** + * @brief Flag to ensure a one-shot log message is printed when watering is skipped. + */ +static bool wateringSkippedLogged = false; + +// --------------------------------------------------------------------- +// Auto Mode State Variables (Global) +// --------------------------------------------------------------------- +// These variables are declared as extern in config.h. +extern bool autoMode; // Global auto mode flag. +extern unsigned long lastPHCheckTime; // Timestamp for the last pH check (in ms). + +// --------------------------------------------------------------------- +// Per-Plant Configuration Note +// --------------------------------------------------------------------- +// The per-plant watering configuration is stored in the global array: +// extern PlantWateringConfig plantConfigs[3]; +// which is defined in config.h and in the main sketch. The structure is defined as: +// +// struct WateringSetpoints { +// float soilMoistureLow; +// float soilMoistureHigh; +// }; +// +// struct PlantWateringConfig { +// WateringSetpoints setpoints[4]; // Indexes: 0 = seedling, 1 = vegetative, +// // 2 = flowering, 3 = drying. +// uint8_t currentMode; // Active watering mode (0–3). +// }; +// +// The default configuration is provided via DEFAULT_PLANT_CONFIGS in config.h. + +// --------------------------------------------------------------------- +// applyWateringMode() +// --------------------------------------------------------------------- +/** + * @brief Log the active moisture thresholds for a plant if they have changed. + * + * For the given plant (indexed by plantIndex), this function reads the active watering mode + * (plantConfigs[plantIndex].currentMode) and obtains the corresponding low and high moisture thresholds. + * If these thresholds differ from the last logged values, it logs them to Serial and updates the + * lastLow and lastHigh arrays. + * + * @param plantIndex Zero-based index of the plant. + */ +inline void applyWateringMode(size_t plantIndex) { + // Determine the active watering mode. + uint8_t mode = plantConfigs[plantIndex].currentMode; + int newLow = (int)plantConfigs[plantIndex].setpoints[mode].soilMoistureLow; + int newHigh = (int)plantConfigs[plantIndex].setpoints[mode].soilMoistureHigh; + + // Log only if the thresholds have changed. + if (newLow != lastLow[plantIndex] || newHigh != lastHigh[plantIndex]) { + Serial.print(F("Plant ")); + Serial.print(plantIndex + 1); + Serial.print(F(" active thresholds: Low=")); + Serial.print(newLow); + Serial.print(F(", High=")); + Serial.println(newHigh); + lastLow[plantIndex] = newLow; + lastHigh[plantIndex] = newHigh; + } +} + +// --------------------------------------------------------------------- +// manageMixingTank() +// --------------------------------------------------------------------- +/** + * @brief Controls the fill pump for the mixing tank. + * + * This function turns the fill pump ON if the mixer tank level is below the refill threshold + * and the feeder tank has sufficient water, and it resets the pH balanced flag to indicate that + * a new fill cycle has begun. The fill pump is turned OFF when the mixer tank reaches full level + * or the feeder tank is too low. + */ +inline void manageMixingTank() { + bool newState = lastMixPumpState; + + // Activate fill pump if mixer tank is below the refill threshold and feeder tank is adequate. + if (mixerLevel <= mixerRefillLevel && feederLevel > feederLevelSetpoint) { + newState = LOW; // Active-low: LOW means pump ON. + phBalanced = false; // Start a new fill cycle; pH will need balancing. + } + // Deactivate fill pump if mixer tank is full or feeder tank is low. + if (mixerLevel >= mixerFullLevel || feederLevel <= feederLevelSetpoint) { + newState = HIGH; // Turn pump OFF. + } + + // If there is a change in pump state, update the output and log the change. + if (newState != lastMixPumpState) { + if (newState == LOW) { + Serial.println(F("Mixing Tank: Starting refill...")); + } else { + Serial.println(F("Mixing Tank: Full or feeder empty - Stopping fill pump")); + } + digitalWrite(PUMP_PINS[3], newState); + lastMixPumpState = newState; + } +} + +// --------------------------------------------------------------------- +// adjustPH() +// --------------------------------------------------------------------- +/** + * @brief Adjusts the pH of the mixing tank once per fill cycle. + * + * If the mixer tank is full and the pH has not yet been balanced in the current cycle, + * this function waits for the pH settling period and then reads the pH sensor. + * If the pH exceeds the target maximum, it activates the pH pump followed by the mixer pump, + * and logs the adjustment process. Once complete, the pH is marked as balanced. + */ +inline void adjustPH() { + // Only check and adjust pH if the mixer tank is full. + if (mixerLevel < mixerFullLevel) return; + if (phBalanced) return; // pH already balanced in this cycle. + if (millis() - lastPHCheckTime < phSettlingTime * 1000UL) return; // Wait for the settling period. + + float currentPH = readPHSensor(); + if (currentPH > phTargetMax) { + Serial.println(F("pH too high - Initiating pH balance routine...")); + digitalWrite(PUMP_PINS[4], LOW); // Activate pH pump. + delay(phPumpDuration * 1000UL); + digitalWrite(PUMP_PINS[4], HIGH); // Deactivate pH pump. + + digitalWrite(PUMP_PINS[5], LOW); // Activate mixer pump. + delay(mixingPumpDuration * 1000UL); + digitalWrite(PUMP_PINS[5], HIGH); // Deactivate mixer pump. + + Serial.println(F("pH adjusted - Chemical added, mixing complete.")); + } else { + Serial.println(F("pH acceptable; no adjustment needed.")); + } + + lastPHCheckTime = millis(); // Update the pH check timestamp. + phBalanced = true; // Mark the current fill cycle as pH balanced. +} + +// --------------------------------------------------------------------- +// managePlantWatering() +// --------------------------------------------------------------------- +/** + * @brief Controls plant watering based on moisture sensor readings. + * + * This function checks whether the conditions are right for watering: + * - The mixing tank pH is balanced. + * - The fill pump is off. + * - The mixer tank is not empty. + * + * If any condition is not met, watering is skipped with a one-shot log message. + * Otherwise, for each plant, the function reads the moisture sensor value, + * compares it against the active thresholds for that plant, and controls the + * corresponding pump accordingly. + */ +inline void managePlantWatering() { + // Skip watering if the mixer tank is not ready. + if (!phBalanced || lastMixPumpState == LOW || mixerLevel <= 0) { + if (!wateringSkippedLogged) { + Serial.println(F("Skipping plant watering: Mixer tank not ready (pH unbalanced, filling, or empty).")); + wateringSkippedLogged = true; + } + return; + } else { + wateringSkippedLogged = false; + } + + // Process watering for each plant. + for (size_t i = 0; i < sizeof(MOISTURE_PINS) / sizeof(MOISTURE_PINS[0]); i++) { + applyWateringMode(i); + int moistureValue = analogRead(MOISTURE_PINS[i]); + bool newPumpState = lastPlantPumpState[i]; + + // Retrieve the active thresholds for this plant based on its current watering mode. + uint8_t mode = plantConfigs[i].currentMode; + int thresholdLow = (int)plantConfigs[i].setpoints[mode].soilMoistureLow; + int thresholdHigh = (int)plantConfigs[i].setpoints[mode].soilMoistureHigh; + + // Determine pump state based on moisture reading. + if (moistureValue < thresholdLow) { + newPumpState = LOW; // Turn pump ON (active-low). + } + if (moistureValue > thresholdHigh) { + newPumpState = HIGH; // Turn pump OFF. + } + + // If the pump state has changed, update the output and log the action. + if (newPumpState != lastPlantPumpState[i]) { + if (newPumpState == LOW) { + Serial.print(F("Plant ")); + Serial.print(i + 1); + Serial.println(F(" - Soil too dry: Watering plant")); + } else { + Serial.print(F("Plant ")); + Serial.print(i + 1); + Serial.println(F(" - Soil moisture adequate: Stopping pump")); + } + digitalWrite(PUMP_PINS[i], newPumpState); + lastPlantPumpState[i] = newPumpState; + } + } +} + +// --------------------------------------------------------------------- +// runAutoMode() +// --------------------------------------------------------------------- +/** + * @brief Executes the auto mode control routines. + * + * If auto mode is enabled, this function logs the transition into auto mode once, + * then sequentially: + * 1. Manages the mixing tank (controlling the fill pump). + * 2. Adjusts the pH (if the mixer tank is full and not yet balanced). + * 3. Manages plant watering (if the mixer tank is ready). + * + * If auto mode is disabled, it resets the one-shot log flag. + */ +inline void runAutoMode() { + if (!autoMode) { + autoModeLogged = false; // Reset the log flag if auto mode is off. + return; + } + + if (!autoModeLogged) { + Serial.println(F("Entering Auto Mode...")); + autoModeLogged = true; + } + + manageMixingTank(); + adjustPH(); + managePlantWatering(); +} + +#endif // AUTO_H diff --git a/bluetooth.h b/bluetooth.h new file mode 100644 index 0000000..d066c36 --- /dev/null +++ b/bluetooth.h @@ -0,0 +1,110 @@ +/** + * @file bluetooth.h + * @brief Bluetooth interface for configuration and status reporting. + * + * This module initializes the Bluetooth serial interface for remote configuration. + * It provides functions to set up Bluetooth, process incoming Bluetooth commands, + * and send periodic status updates (including auto mode, WiFi, and MQTT connectivity). + * + * The status update string is built dynamically by querying the current system state. + * + * @author + * James C. Alexander + * @date + * 2025-02-03 + */ + +#ifndef BLUETOOTH_H +#define BLUETOOTH_H + +#include +#include "config.h" +#include "commands.h" +#include "custom_mqtt.h" +#include + +// --------------------------------------------------------------------- +// Global Bluetooth Serial Object +// --------------------------------------------------------------------- +/** + * @brief Bluetooth Serial interface. + * + * This object is used to communicate over Bluetooth, allowing remote configuration + * and status monitoring. + */ +BluetoothSerial SerialBT; + +/** + * @brief Timestamp for the last Bluetooth status update. + */ +unsigned long lastBTStatusUpdate = 0; + +// --------------------------------------------------------------------- +// Function Prototypes +// --------------------------------------------------------------------- +/** + * @brief Initialize the Bluetooth interface. + * + * Sets up the Bluetooth serial connection with a predefined device name. + */ +void setupBluetooth(); + +/** + * @brief Process incoming Bluetooth commands. + * + * Checks if any data is available on the Bluetooth serial interface. If a command is received, + * it trims any extraneous whitespace and passes the command to the configuration command handler. + */ +void handleBluetoothCommands(); + +/** + * @brief Send periodic status updates over Bluetooth. + * + * If at least 1 second has elapsed since the last update, this function sends a status update + * string via Bluetooth. + */ +void sendBTStatusUpdate(); + +/** + * @brief Build a formatted system status update string. + * + * The status update includes the current auto mode state, WiFi connectivity, and MQTT connectivity. + * + * @return A formatted String containing the system status. + */ +String getStatusUpdate() { + String status = "AutoMode: "; + status += autoMode ? "ON" : "OFF"; + status += ", WiFi: "; + status += (WiFi.status() == WL_CONNECTED) ? "Connected" : "Disconnected"; + status += ", MQTT: "; + status += (client.connected()) ? "Connected" : "Disconnected"; + return status; +} + +// --------------------------------------------------------------------- +// Function Implementations +// --------------------------------------------------------------------- +void setupBluetooth() { + SerialBT.begin("GrowTentController"); // Initialize Bluetooth with the device name. + Serial.println("Bluetooth Started. Ready for configuration."); +} + +void handleBluetoothCommands() { + // If data is available on the Bluetooth interface, read and process the command. + if (SerialBT.available()) { + String command = SerialBT.readStringUntil('\n'); + command.trim(); // Remove any leading/trailing whitespace. + handleConfigCommand(command); + } +} + +void sendBTStatusUpdate() { + // Send a status update every 1000 milliseconds (1 second). + if (millis() - lastBTStatusUpdate >= 1000) { + lastBTStatusUpdate = millis(); + SerialBT.println(getStatusUpdate()); + } +} + +#endif // BLUETOOTH_H diff --git a/commands.h b/commands.h new file mode 100644 index 0000000..fe70108 --- /dev/null +++ b/commands.h @@ -0,0 +1,410 @@ +/** + * @file commands.h + * @brief Serial command processing for configuration updates. + * + * This module handles incoming commands over the Serial interface to update + * configuration variables. Supported commands include: + * + * Global configuration: + * - SET + * e.g., "SET publishInterval 15000" + * + * WiFi configuration: + * - SET_WIFI , + * + * MQTT configuration: + * - SET_MQTT_HOST + * - SET_MQTT_PORT + * - SET_MQTT_USER + * - SET_MQTT_PASS + * - SET_MQTT_TOPIC_ROOT + * + * Per-plant configuration: + * - SET PLANT MODE + * - SET PLANT SETPOINTS + * + * Additionally, a new command is provided to toggle relay logic: + * - SET relayLogic + * (HIGH for active-high relays, LOW for active-low; default is LOW.) + * + * After processing any command, the updated configuration is saved to EEPROM. + * + * Author: James C. Alexander + * Date: 2025-02-03 + */ + +#ifndef COMMANDS_H +#define COMMANDS_H + +#include "config.h" +#include "config_storage.h" +#include +#include +#include +#include "wifi_interface.h" + +// --------------------------------------------------------------------- +// External Objects and Variables +// --------------------------------------------------------------------- +extern BluetoothSerial SerialBT; ///< Bluetooth serial interface. +extern bool wifiEnabled; ///< Flag to enable/disable WiFi reconnect loop. + +// --------------------------------------------------------------------- +// Helper Function: startsWithIgnoreCase +// --------------------------------------------------------------------- +inline bool startsWithIgnoreCase(String str, String prefix) { + str.toLowerCase(); + prefix.toLowerCase(); + return str.startsWith(prefix); +} + +// --------------------------------------------------------------------- +// handleConfigCommand() +// --------------------------------------------------------------------- +inline void handleConfigCommand(String command) { + command.trim(); + + // ----------------------- + // Handle Reboot Commands + // ----------------------- + if (startsWithIgnoreCase(command, "REBOOT") || startsWithIgnoreCase(command, "RESTART")) { + Serial.println("Reboot command received! Restarting ESP32..."); + delay(1000); + ESP.restart(); + return; + } + if (command.startsWith("SET_DEFAULT")) { + saveDefaultConfigToEEPROM(); + Serial.println("Defaults Restored."); + Serial.println("Reboot command received! Restarting ESP32..."); + delay(1000); + ESP.restart(); + return; + } + // ----------------------- + // Handle WiFi Commands + // ----------------------- + if (command.startsWith("SET_WIFI ")) { + String creds = command.substring(9); + int separator = creds.indexOf(','); + if (separator > 0) { + String newSSID = creds.substring(0, separator); + String newPassword = creds.substring(separator + 1); + Serial.println("Updating WiFi credentials..."); + if (WiFi.status() == WL_CONNECTED) { + WiFi.disconnect(); + Serial.println("WiFi disconnected for credential update."); + } + newSSID.toCharArray(ssid, sizeof(ssid)); + newPassword.toCharArray(password, sizeof(password)); + saveConfigToEEPROM(); + Serial.print("WiFi credentials updated: SSID="); + Serial.println(ssid); + Serial.println("Please issue WIFI_CONNECT to restart connection."); + } + return; + } + if (startsWithIgnoreCase(command, "SCAN_WIFI")) { + bool jsonOutput = command.length() > 10 && startsWithIgnoreCase(command.substring(10), "JSON"); + Serial.println("Scanning for WiFi networks..."); + int networkCount = WiFi.scanNetworks(); + if (networkCount < 0) { + Serial.printf("WiFi scan failed! Error code: %d\n", networkCount); + return; + } + if (networkCount == 0) { + Serial.println("No WiFi networks found."); + return; + } + if (jsonOutput) { + DynamicJsonDocument jsonDoc(1024); + JsonArray networks = jsonDoc.createNestedArray("networks"); + for (int i = 0; i < networkCount; i++) { + JsonObject net = networks.createNestedObject(); + net["ssid"] = WiFi.SSID(i); + net["signal"] = WiFi.RSSI(i); + net["security"] = (WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? "Open" : "Protected"; + } + String jsonString; + serializeJson(jsonDoc, jsonString); + Serial.println(jsonString); + } else { + Serial.printf("Found %d networks:\n", networkCount); + for (int i = 0; i < networkCount; i++) { + Serial.printf("%d: SSID: %s | Signal: %d dBm | Security: %s\n", + i + 1, + WiFi.SSID(i).c_str(), + WiFi.RSSI(i), + WiFi.encryptionType(i) == WIFI_AUTH_OPEN ? "Open" : "Protected"); + } + } + WiFi.scanDelete(); + return; + } + // ----------------------- + // Handle MQTT Commands + // ----------------------- + if (command.startsWith("SET_MQTT_HOST ")) { + String newHost = command.substring(14); + newHost.trim(); + if (newHost.length() > 0) { + newHost.toCharArray(mqtt_host, sizeof(mqtt_host)); + saveConfigToEEPROM(); + Serial.print("MQTT host updated to: "); + Serial.println(mqtt_host); + } + return; + } + if (command.startsWith("SET_MQTT_PORT ")) { + String portStr = command.substring(14); + portStr.trim(); + uint16_t newPort = portStr.toInt(); + if (newPort > 0) { + mqtt_port = newPort; + saveConfigToEEPROM(); + Serial.print("MQTT port updated to: "); + Serial.println(mqtt_port); + } + return; + } + if (command.startsWith("SET_MQTT_USER ")) { + String newUser = command.substring(14); + newUser.trim(); + newUser.toCharArray(mqtt_username, sizeof(mqtt_username)); + saveConfigToEEPROM(); + Serial.print("MQTT username updated to: "); + Serial.println(mqtt_username); + return; + } + if (command.startsWith("SET_MQTT_PASS ")) { + String newPass = command.substring(14); + newPass.trim(); + newPass.toCharArray(mqtt_password, sizeof(mqtt_password)); + saveConfigToEEPROM(); + Serial.print("MQTT password updated to: "); + Serial.println(mqtt_password); + return; + } + if (command.startsWith("SET_MQTT_TOPIC_ROOT ")) { + String newRoot = command.substring(20); + newRoot.trim(); + newRoot.toCharArray(mqtt_topic_root, sizeof(mqtt_topic_root)); + saveConfigToEEPROM(); + Serial.print("MQTT topic root updated to: "); + Serial.println(mqtt_topic_root); + return; + } + if (startsWithIgnoreCase(command, "SET_MQTT_CONN ")) { + String params = command.substring(14); + params.trim(); + int idx1 = params.indexOf(','); + int idx2 = params.indexOf(',', idx1 + 1); + int idx3 = params.indexOf(',', idx2 + 1); + int idx4 = params.indexOf(',', idx3 + 1); + if (idx1 == -1 || idx2 == -1 || idx3 == -1 || idx4 == -1) { + Serial.println("Invalid MQTT connection format. Expected:"); + Serial.println("SET_MQTT_CONN ,,,,"); + return; + } + String newHost = params.substring(0, idx1); + String newPortStr = params.substring(idx1 + 1, idx2); + String newUser = params.substring(idx2 + 1, idx3); + String newPass = params.substring(idx3 + 1, idx4); + String newTopicRoot = params.substring(idx4 + 1); + newHost.trim(); + newPortStr.trim(); + newUser.trim(); + newPass.trim(); + newTopicRoot.trim(); + uint16_t newPort = newPortStr.toInt(); + if (newPort <= 0) { + Serial.println("Invalid MQTT port. Must be a number greater than 0."); + return; + } + newHost.toCharArray(mqtt_host, sizeof(mqtt_host)); + mqtt_port = newPort; + newUser.toCharArray(mqtt_username, sizeof(mqtt_username)); + newPass.toCharArray(mqtt_password, sizeof(mqtt_password)); + newTopicRoot.toCharArray(mqtt_topic_root, sizeof(mqtt_topic_root)); + saveConfigToEEPROM(); + Serial.println("MQTT settings updated:"); + Serial.print("Host: "); Serial.println(mqtt_host); + Serial.print("Port: "); Serial.println(mqtt_port); + Serial.print("User: "); Serial.println(mqtt_username); + Serial.print("Password: "); Serial.println("********"); + Serial.print("Topic Root: "); Serial.println(mqtt_topic_root); + return; + } + // ----------------------- + // Handle Global Configuration SET Command + // ----------------------- + if (command.startsWith("SET ")) { + int firstSpace = command.indexOf(' '); + int secondSpace = command.indexOf(' ', firstSpace + 1); + if (secondSpace < 0) { + Serial.println("Invalid SET command format. Expected: SET "); + return; + } + String param = command.substring(firstSpace + 1, secondSpace); + String value = command.substring(secondSpace + 1); + value.trim(); + if (param.equalsIgnoreCase("publishInterval")) + publishInterval = value.toInt(); + else if (param.equalsIgnoreCase("wifiReconnectInterval")) + wifiReconnectInterval = value.toInt(); + else if (param.equalsIgnoreCase("mqttReconnectInterval")) + mqttReconnectInterval = value.toInt(); + else if (param.equalsIgnoreCase("mixerLevelSetpoint")) + mixerLevelSetpoint = value.toFloat(); + else if (param.equalsIgnoreCase("feederLevelSetpoint")) + feederLevelSetpoint = value.toFloat(); + else if (param.equalsIgnoreCase("phSettlingTime")) + phSettlingTime = value.toFloat(); + else if (param.equalsIgnoreCase("phTargetMax")) + phTargetMax = value.toFloat(); + else if (param.equalsIgnoreCase("phPumpDuration")) + phPumpDuration = value.toFloat(); + else if (param.equalsIgnoreCase("mixingPumpDuration")) + mixingPumpDuration = value.toFloat(); + else if (param.equalsIgnoreCase("autoMode")) + autoMode = (value == "1" || value.equalsIgnoreCase("true")); + else if (param.equalsIgnoreCase("bypassFillInterlock")) + bypassFillInterlock = (value == "1" || value.equalsIgnoreCase("true")); + else { + Serial.print("Unknown global parameter: "); + Serial.println(param); + return; + } + saveConfigToEEPROM(); + Serial.print("Global parameter updated: "); + Serial.print(param); + Serial.print(" = "); + Serial.println(value); + return; + } + // ----------------------- + // Handle Per-Plant Configuration Commands + // ----------------------- + if (command.startsWith("SET PLANT")) { + String remainder = command.substring(9); + remainder.trim(); + int spacePos = remainder.indexOf(' '); + if (spacePos < 0) { + Serial.println("Invalid per-plant command format."); + return; + } + String plantStr = remainder.substring(0, spacePos); + int plantIndex = plantStr.toInt() - 1; + if (plantIndex < 0 || plantIndex >= 3) { + Serial.println("Plant index out of range."); + return; + } + remainder = remainder.substring(spacePos + 1); + remainder.trim(); + if (remainder.startsWith("MODE ")) { + String modeVal = remainder.substring(5); + modeVal.trim(); + plantConfigs[plantIndex].currentMode = modeVal.toInt(); + saveConfigToEEPROM(); + Serial.print("Plant "); + Serial.print(plantIndex + 1); + Serial.print(" active mode updated to: "); + Serial.println(plantConfigs[plantIndex].currentMode); + return; + } else if (remainder.startsWith("SETPOINTS ")) { + String params = remainder.substring(10); + params.trim(); + int firstSp = params.indexOf(' '); + int secondSp = params.indexOf(' ', firstSp + 1); + int thirdSp = params.indexOf(' ', secondSp + 1); + if (firstSp < 0 || secondSp < 0 || thirdSp < 0) { + Serial.println("Invalid SETPOINTS command format. Expected: SETPOINTS "); + return; + } + String modeName = params.substring(0, firstSp); + String threshType = params.substring(firstSp + 1, secondSp); + String valueStr = params.substring(secondSp + 1); + valueStr.trim(); + int modeIndex = -1; + if (modeName.equalsIgnoreCase("seedling")) + modeIndex = SEEDLING_MODE; + else if (modeName.equalsIgnoreCase("vegetative")) + modeIndex = VEGETATIVE_MODE; + else if (modeName.equalsIgnoreCase("flowering")) + modeIndex = FLOWERING_MODE; + else if (modeName.equalsIgnoreCase("drying")) + modeIndex = DRYING_MODE; + else { + Serial.print("Unknown watering mode: "); + Serial.println(modeName); + return; + } + float newValue = valueStr.toFloat(); + if (threshType.equalsIgnoreCase("LOW")) { + plantConfigs[plantIndex].setpoints[modeIndex].soilMoistureLow = newValue; + Serial.print("Plant "); + Serial.print(plantIndex + 1); + Serial.print(" mode "); + Serial.print(modeName); + Serial.print(" low setpoint updated to: "); + Serial.println(newValue); + } else if (threshType.equalsIgnoreCase("HIGH")) { + plantConfigs[plantIndex].setpoints[modeIndex].soilMoistureHigh = newValue; + Serial.print("Plant "); + Serial.print(plantIndex + 1); + Serial.print(" mode "); + Serial.print(modeName); + Serial.print(" high setpoint updated to: "); + Serial.println(newValue); + } else { + Serial.print("Unknown threshold type: "); + Serial.println(threshType); + return; + } + saveConfigToEEPROM(); + return; + } else { + Serial.println("Unknown per-plant command format."); + return; + } + } + + // ----------------------- + // New Command: Toggle Relay Logic + // ----------------------- + if (startsWithIgnoreCase(command, "SET relayLogic ")) { + String value = command.substring(15); + value.trim(); + if (value.equalsIgnoreCase("HIGH") || value.equalsIgnoreCase("true")) { + relayActiveHigh = true; + Serial.println("Relay activation set to ACTIVE-HIGH."); + } else if (value.equalsIgnoreCase("LOW") || value.equalsIgnoreCase("false")) { + relayActiveHigh = false; + Serial.println("Relay activation set to ACTIVE-LOW."); + } else { + Serial.println("Invalid relayLogic value. Use 'HIGH' or 'LOW'."); + } + saveConfigToEEPROM(); + return; + } + + // If no command matched + Serial.println("Unknown command."); +} + +// --------------------------------------------------------------------- +// processSerialCommands() +// --------------------------------------------------------------------- +inline void processSerialCommands() { + if (Serial.available() > 0) { + String command = Serial.readStringUntil('\n'); + command.trim(); + if (command.length() > 0) { + Serial.print("Received command: "); + Serial.println(command); + handleConfigCommand(command); + } + } +} + +#endif // COMMANDS_H diff --git a/commands.md b/commands.md new file mode 100644 index 0000000..3274e90 --- /dev/null +++ b/commands.md @@ -0,0 +1,58 @@ +# 🌱 ESP32 Greenhouse Controller - Command Reference + +## ⚡ System Commands +| **Command** | **Description** | +|------------------------|----------------| +| `REBOOT` / `RESTART` | Reboots the ESP32 immediately. | +| `SET_DEFAULT` | Restores all configuration to default and reboots the ESP32. | + +## 📡 WiFi Commands +| **Command** | **Description** | +|----------------------------------|----------------| +| `SET_WIFI ,` | Updates WiFi credentials & saves them to EEPROM. | +| `SCAN_WIFI` | Scans for available WiFi networks (human-readable). | +| `SCAN_WIFI JSON` | Scans for WiFi networks and returns results in JSON format. | + +## 📡 MQTT Commands +| **Command** | **Description** | +|--------------------------------------|----------------| +| `SET_MQTT_HOST ` | Updates MQTT broker hostname or IP. | +| `SET_MQTT_PORT ` | Updates MQTT broker port. | +| `SET_MQTT_USER ` | Updates MQTT username. | +| `SET_MQTT_PASS ` | Updates MQTT password. | +| `SET_MQTT_TOPIC_ROOT ` | Updates MQTT topic root. | +| `SET_MQTT_CONN ,,,,` | Sets all MQTT settings in one command. | + +## ⚙️ Global Configuration Commands +| **Command** | **Description** | +|--------------------------------|----------------| +| `SET ` | Updates a global configuration parameter. | +| **Supported Parameters** | `publishInterval`, `wifiReconnectInterval`, `mqttReconnectInterval`, `mixerLevelSetpoint`, `feederLevelSetpoint`, `mixerFullLevel`, `mixerRefillLevel`, `phSettlingTime`, `phTargetMax`, `phPumpDuration`, `mixingPumpDuration`, `autoMode`, `bypassFillInterlock` | + +## 🌱 Per-Plant Configuration Commands +| **Command** | **Description** | +|-----------------------------------------------|----------------| +| `SET PLANT MODE ` | Sets the active watering mode for a plant (index starts at 1). | +| `SET PLANT SETPOINTS ` | Updates moisture setpoints for a plant's growth stage. | + +## 🔹 Example Usages: +``` +SET_WIFI MyHomeWiFi,SecurePassword123 +``` +**Output:** +``` +WiFi credentials updated: SSID=MyHomeWiFi +``` + +``` +SCAN_WIFI JSON +``` +**Output:** +```json +{ + "networks": [ + { "ssid": "HomeWiFi", "signal": -67, "security": "Protected" }, + { "ssid": "GuestWiFi", "signal": -72, "security": "Protected" } + ] +} +``` diff --git a/config.h b/config.h new file mode 100644 index 0000000..09a8456 --- /dev/null +++ b/config.h @@ -0,0 +1,208 @@ +/** + * @file config.h + * @brief Global configuration definitions for the GreenHouse Controller. + * + * This file defines default values for WiFi, MQTT, timing parameters, setpoints, + * and per-plant watering configurations. It also declares modifiable global variables, + * which are defined in one source file (typically the main sketch). Additionally, + * inline functions are provided to dynamically build MQTT topics based on the current + * configuration. + * + * Author: James C. Alexander + * Date: 2025-02-03 + */ + +#ifndef CONFIG_H +#define CONFIG_H + +#include +#include +#include + +// --------------------------------------------------------------------- +// Global Default Values +// --------------------------------------------------------------------- +#define DEFAULT_SSID "" ///< Default WiFi SSID. +#define DEFAULT_WIFI_PASSWORD "" ///< Default WiFi password. +#define DEFAULT_MQTT_HOST "000.000.000.000" ///< Default MQTT broker host. +#define DEFAULT_MQTT_PORT 1883 ///< Default MQTT broker port. +#define DEFAULT_MQTT_USERNAME "" ///< Default MQTT username (empty if not used). +#define DEFAULT_MQTT_PASSWORD "" ///< Default MQTT password (empty if not used). +#define DEFAULT_MQTT_TOPIC_ROOT "GroPro" ///< Default MQTT topic root. + +#define DEFAULT_PUBLISH_INTERVAL 2500 ///< Default publish interval in milliseconds. +#define DEFAULT_WIFI_RECONNECT_INTERVAL 5000 ///< Default WiFi reconnect interval in milliseconds. +#define DEFAULT_MQTT_RECONNECT_INTERVAL 5000 ///< Default MQTT reconnect interval in milliseconds. +#define DEFAULT_MIXER_LEVEL_SETPOINT 50.0 ///< Default mixer tank refill setpoint (cm). +#define DEFAULT_FEEDER_LEVEL_SETPOINT 10.0 ///< Default feeder tank refill setpoint (cm). +#define DEFAULT_MIXER_FULL_LEVEL 80.0 ///< Default mixer tank full level (cm). +#define DEFAULT_MIXER_REFILL_LEVEL 40.0 ///< Default mixer tank refill level (cm). +#define DEFAULT_PH_SETTLING_TIME 5.0 ///< Default delay after mixing before checking pH (seconds). +#define DEFAULT_PH_TARGET_MAX 6.5 ///< Default maximum acceptable pH. +#define DEFAULT_PH_PUMP_DURATION 3.0 ///< Default duration to run the pH pump (seconds). +#define DEFAULT_MIXING_PUMP_DURATION 10.0 ///< Default duration to run the mixing pump (seconds). + +// --------------------------------------------------------------------- +// Watering Modes and Per-Plant Watering Configuration +// --------------------------------------------------------------------- +/** + * @enum WateringMode + * @brief Defines the various watering modes. + */ +enum WateringMode { + SEEDLING_MODE = 0, + VEGETATIVE_MODE = 1, + FLOWERING_MODE = 2, + DRYING_MODE = 3 +}; + +/** + * @struct WateringSetpoints + * @brief Holds the soil moisture setpoints for a specific watering mode. + */ +struct WateringSetpoints { + float soilMoistureLow; ///< Low threshold for soil moisture. + float soilMoistureHigh; ///< High threshold for soil moisture. +}; + +/** + * @struct PlantWateringConfig + * @brief Holds the complete watering configuration for a single plant. + */ +struct PlantWateringConfig { + WateringSetpoints setpoints[4]; ///< Setpoints for each watering mode. + uint8_t currentMode; ///< Active watering mode index (0-3). +}; + +/** + * @brief Default watering configurations for three plants. + */ +const PlantWateringConfig DEFAULT_PLANT_CONFIGS[3] = { + { { {60, 80}, {40, 70}, {30, 60}, {20, 40} }, VEGETATIVE_MODE }, + { { {60, 80}, {40, 70}, {30, 60}, {20, 40} }, VEGETATIVE_MODE }, + { { {60, 80}, {40, 70}, {30, 60}, {20, 40} }, VEGETATIVE_MODE } +}; + +// --------------------------------------------------------------------- +// WiFi Configuration +// --------------------------------------------------------------------- +/** + * @brief Modifiable WiFi configuration buffers. + */ +extern char ssid[32]; ///< WiFi SSID. +extern char password[64]; ///< WiFi password. + +// --------------------------------------------------------------------- +// MQTT Configuration +// --------------------------------------------------------------------- +/** + * @brief Modifiable MQTT broker settings. + */ +extern char mqtt_host[64]; ///< MQTT broker host. +extern uint16_t mqtt_port; ///< MQTT broker port. +extern char mqtt_username[32]; ///< MQTT username. +extern char mqtt_password[32]; ///< MQTT password. +extern char mqtt_topic_root[32]; ///< MQTT topic root. + +// --------------------------------------------------------------------- +// Inline Functions for Dynamic Topic Construction +// --------------------------------------------------------------------- +inline String getMqttConfigTopic() { + return String(mqtt_topic_root) + "/config"; +} +inline String getMqttPublishIntervalTopic() { + return String(mqtt_topic_root) + "/config/pubInt"; +} +inline String getMqttWifiReconnectTopic() { + return String(mqtt_topic_root) + "/config/wifInt"; +} +inline String getMqttMqttReconnectTopic() { + return String(mqtt_topic_root) + "/config/mqtInt"; +} +inline String getMqttMixerLevelSetpointTopic() { + return String(mqtt_topic_root) + "/config/mixSet"; +} +inline String getMqttFeederLevelSetpointTopic() { + return String(mqtt_topic_root) + "/config/feeSet"; +} +inline String getMqttBypassFillInterlockTopic() { + return String(mqtt_topic_root) + "/config/bypass"; +} +inline String getMqttWateringModeTopic(uint8_t plantIndex) { + return String(mqtt_topic_root) + "/config/plant" + String(plantIndex + 1) + "/wMode"; +} +inline String getMqttSoilMoistureLowTopic(uint8_t plantIndex) { + return String(mqtt_topic_root) + "/config/plant" + String(plantIndex + 1) + "/sLow"; +} +inline String getMqttSoilMoistureHighTopic(uint8_t plantIndex) { + return String(mqtt_topic_root) + "/config/plant" + String(plantIndex + 1) + "/sHigh"; +} +inline String getAmbientTempTopic() { + return String(mqtt_topic_root) + "/climate/temp"; +} +inline String getAmbientHumidityTopic() { + return String(mqtt_topic_root) + "/climate/hum"; +} +inline String getPhTopic() { + return String(mqtt_topic_root) + "/mixing/ph"; +} +inline String getTdsTopic() { + return String(mqtt_topic_root) + "/mixing/tds"; +} +inline String getPhotoTopic() { + return String(mqtt_topic_root) + "/lighting/photo"; +} +inline String getFeederLevelTopic() { + return String(mqtt_topic_root) + "/tank/feeder"; +} +inline String getMixerLevelTopic() { + return String(mqtt_topic_root) + "/tank/mixer"; +} + +// --------------------------------------------------------------------- +// Global Configuration Variables (Stored in EEPROM) +// --------------------------------------------------------------------- +extern unsigned long publishInterval; +extern unsigned long wifiReconnectInterval; +extern unsigned long mqttReconnectInterval; +extern float mixerLevelSetpoint; +extern float feederLevelSetpoint; + +// --------------------------------------------------------------------- +// Additional Global Configuration Variables +// --------------------------------------------------------------------- +extern float mixerFullLevel; +extern float mixerRefillLevel; +extern float phSettlingTime; +extern float phTargetMax; +extern float phPumpDuration; +extern float mixingPumpDuration; + +// --------------------------------------------------------------------- +// Auto Mode and Interlock Flags +// --------------------------------------------------------------------- +extern bool autoMode; +extern bool bypassFillInterlock; + +// --------------------------------------------------------------------- +// Tank Level Sensor Readings +// --------------------------------------------------------------------- +extern float mixerLevel; +extern float feederLevel; + +// --------------------------------------------------------------------- +// Per-Plant Watering Configuration Variables +// --------------------------------------------------------------------- +extern PlantWateringConfig plantConfigs[3]; + +// --------------------------------------------------------------------- +// New Global Variable for Relay Logic +// --------------------------------------------------------------------- +/** + * @brief Relay activation logic flag. + * false: relays are active-low (default) + * true: relays are active-high. + */ +extern bool relayActiveHigh; + +#endif // CONFIG_H