/** * @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