New files

new files
This commit is contained in:
psi2100 2025-02-10 06:04:18 +00:00
commit 474f0ec63a
5 changed files with 1086 additions and 0 deletions

300
auto.h Normal file
View File

@ -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 (03).
// };
//
// 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

110
bluetooth.h Normal file
View File

@ -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 <BluetoothSerial.h>
#include "config.h"
#include "commands.h"
#include "custom_mqtt.h"
#include <WiFi.h>
// ---------------------------------------------------------------------
// 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

410
commands.h Normal file
View File

@ -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 <parameter> <value>
* e.g., "SET publishInterval 15000"
*
* WiFi configuration:
* - SET_WIFI <SSID>,<Password>
*
* MQTT configuration:
* - SET_MQTT_HOST <host>
* - SET_MQTT_PORT <port>
* - SET_MQTT_USER <username>
* - SET_MQTT_PASS <password>
* - SET_MQTT_TOPIC_ROOT <root>
*
* Per-plant configuration:
* - SET PLANT<index> MODE <value>
* - SET PLANT<index> SETPOINTS <mode> <LOW|HIGH> <value>
*
* Additionally, a new command is provided to toggle relay logic:
* - SET relayLogic <HIGH|LOW>
* (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 <WiFi.h>
#include <BluetoothSerial.h>
#include <ArduinoJson.h>
#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 <host>,<port>,<user>,<password>,<topic_root>");
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 <parameter> <value>");
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 <mode> <LOW|HIGH> <value>");
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

58
commands.md Normal file
View File

@ -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 <SSID>,<PASSWORD>` | 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 <host>` | Updates MQTT broker hostname or IP. |
| `SET_MQTT_PORT <port>` | Updates MQTT broker port. |
| `SET_MQTT_USER <username>` | Updates MQTT username. |
| `SET_MQTT_PASS <password>` | Updates MQTT password. |
| `SET_MQTT_TOPIC_ROOT <root>` | Updates MQTT topic root. |
| `SET_MQTT_CONN <host>,<port>,<user>,<pass>,<root>` | Sets all MQTT settings in one command. |
## ⚙️ Global Configuration Commands
| **Command** | **Description** |
|--------------------------------|----------------|
| `SET <parameter> <value>` | 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<index> MODE <value>` | Sets the active watering mode for a plant (index starts at 1). |
| `SET PLANT<index> SETPOINTS <mode> <LOW|HIGH> <value>` | 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" }
]
}
```

208
config.h Normal file
View File

@ -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 <WiFi.h>
#include <PubSubClient.h>
#include <Preferences.h>
// ---------------------------------------------------------------------
// 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