My journey through the MIT course
Assignment: write an application that interfaces a user with an input &/or output device that you made
This project is an automated irrigation system designed to deliver precise amounts of water to distinct agricultural or garden zones based on real-time soil moisture readings. The primary goal is to optimize water usage, improve plant health by preventing over/under-watering, and automate a crucial aspect of plant care. The system, as of Spiral 4, supports multi-zone monitoring, configurable watering parameters, and basic fail-safe mechanisms.
The system is comprised of several interconnected modules:
graph TD
SM1[Soil Moisture Sensor ESP32 - Zone A] -- HTTP POST JSON --> FAS[FastAPI Server]
SM2[Soil Moisture Sensor ESP32 - Zone B] -- HTTP POST JSON --> FAS
FAS -- HTTP GET JSON --> CCU[Central Control Unit (ESP32/RPi)]
CCU -- Serial Commands --> PUMP[Irrigation Pump ESP32]
rawValue
: Integer ADC reading from the sensor (e.g., ~1200 for wet, ~2800 for dry, varies by sensor and soil).moisturePercent
: Integer (0-100%) representing relative moisture. Calculated on the ESP32 using a linear map function with sensor-specific SENSOR_AIR_VALUE
(0% reference) and SENSOR_WATER_VALUE
(100% reference).sensorID
: A unique string/integer to identify the sensor/zone (e.g., "ZoneA_Veg").moisturePercent
is a relative index, not absolute Volumetric Water Content (VWC). Its accuracy is highly dependent on the initial two-point calibration specific to each sensor and soil type.esp32_calibration_code
.
EN_PIN
(D5 on ESP32): Enables/disables the motor driver (LOW = Enabled).STEP_PIN
(D8 on ESP32): Each pulse advances the motor one step/microstep.DIR_PIN
(D7 on ESP32): Sets the direction of rotation (HIGH for default dispensing).\\n
).
steps <numberOfMicrosteps> <totalPeriodMicroseconds>\\n
: Dispenses volume by executing <numberOfMicrosteps>
. Speed is set by <totalPeriodMicroseconds>
(e.g., 1kHz pulse rate). This command is blocking on the Pump ESP32.motor_enable\\n
: Explicitly enables the motor driver.motor_disable\\n
: Explicitly disables the motor driver (sets EN_PIN HIGH).ACK\\n
or signal completion with DONE\\n
.mL_per_microstep
(e.g., 0.000625 mL/microstep). This value is determined experimentally and is essential for converting a target volume in mL to the required number of motor microsteps.POST /data/soil_moisture/
: Receives JSON data from soil moisture modules. Stores the latest reading per sensorID
along with a reception timestamp.GET /data/soil_moisture/latest/{sensorID}
: Provides the most recent JSON data record for the specified sensorID
to the CCU.sensorID
s.config.json
stored on the CCU's filesystem).moisturePercent
against configured thresholds, considering data freshness and validity.moisturePercent
, and POST
a JSON payload (including sensorID
, rawValue
, moisturePercent
, and timestamp
) to the FastAPI server.GET
request to the FastAPI server for the latest data associated with the zone's sensorID
.rawValue
and moisturePercent
are within expected operational ranges (e.g., 0 <= moisturePercent <= 100
).The system supports multiple distinct irrigation zones, each with its own settings. Configuration is managed via a JSON file (e.g., config.json
) on the CCU, structured similar to this:
[
{
"zoneID": "Zone1_Vegetables",
"sensorID": "SensorA_Veg",
"triggerMoisturePercent": 30,
"targetVolumeML": 500,
"pumpPeriodMicroseconds": 1200,
"mL_per_microstep": 0.000625
},
{
"zoneID": "Zone2_Herbs",
"sensorID": "SensorB_Herbs",
"triggerMoisturePercent": 40,
"targetVolumeML": 250,
"pumpPeriodMicroseconds": 1500,
"mL_per_microstep": 0.000625
}
]
For each zone:
currentMoisturePercent
< zone.triggerMoisturePercent
AND data is fresh AND data is valid:
numberOfMicrosteps = zone.targetVolumeML / zone.mL_per_microstep
.motor_enable\\n
command to Pump ESP32.steps <calculated_numberOfMicrosteps> <zone.pumpPeriodMicroseconds>\\n
command.ACK\\n
or DONE\\n
from pump, or use a timeout).motor_disable\\n
command to Pump ESP32 (either immediately after this zone or after the entire irrigation cycle for all zones).The CCU translates the desired water volume into precise motor commands. The Pump ESP32 focuses solely on executing these commands, handling the low-level motor stepping and timing. The blocking nature of the steps
command on the Pump ESP32 is architecturally handled by keeping the CCU as a separate, non-blocked unit.
rawValue
or moisturePercent
are significantly outside calibrated/expected ranges (e.g., moisturePercent < 0
or rawValue == 0
if unexpected), the CCU treats the data as invalid for that cycle, skips irrigation for the affected zone, and logs the anomaly.POST /data/soil_moisture/
POST
{
"sensorID": "SensorA_Veg",
"rawValue": 1850,
"moisturePercent": 65,
"timestamp": "2025-05-27T18:00:00Z"
}
GET /data/soil_moisture/latest/{sensorID}
(e.g., /data/soil_moisture/latest/SensorA_Veg
)GET
\\n
) terminated.steps <numberOfMicrosteps> <totalPeriodMicroseconds>\\n
steps 160000 1000\\n
(Dispense volume equivalent to 160,000 microsteps at 1000µs per step period).motor_enable\\n
: Activates the stepper motor driver.motor_disable\\n
: Deactivates the stepper motor driver to save power and prevent motor heat/hum.ACK\\n
or DONE\\n
.triggerMoisturePercent
) and dispense volumes (targetVolumeML
) loaded from a config.json
file.EN_PIN
management via motor_enable
/motor_disable
commands for better efficiency and motor lifespan.All source code for the Soil Moisture Sensor ESP32 firmware, Pump Control ESP32 firmware, FastAPI Server application, and the Central Control Unit logic can be found in my project repository:
[Link to Your GitHub Repository Here]
(Consider embedding a Fritzing diagram or a clear photo of your setup here.)
SENSOR_AIR_VALUE
and SENSOR_WATER_VALUE
for each capacitive soil moisture sensor in its specific soil type was critical and iterative. These values significantly impact the moisturePercent
reading.mL_per_microstep
for the pump required careful experimentation and averaging over multiple trials to ensure consistent dispense volumes.steps
command necessitated the architectural decision to have a separate CCU, preventing the entire system from freezing during dispensing.config.json
) greatly improved flexibility and testability for different zones and conditions.Beyond the current Spiral 4 status, planned enhancements include:
This project is an automated vertical linear actuator designed to precisely control the height of an LED grow light panel for an indoor garden system. The primary goal is to maximize the amount of plants that the light handle and optimize light distribution. The system employs a dual NEMA 17 stepper motor configuration, controlled by an ESP32 microcontroller, allowing for an extended range of motion and fine-tuned positioning.
The development followed a spiral methodology, incrementally building and testing functionality from basic motor control to coordinated multi-axis movement.
The actuator is designed with two stepper motors working in tandem along a single vertical axis.
Dual-Stepper Configuration:
Linear Motion: Both motors drive lead screws (e.g., repurposed Prusa MK3 Z-axis TR8*8 lead screws) to convert rotational motion into linear motion. The entire assembly slides along a rigid linear rail to ensure stability and smooth travel.
Payload: A sideways-mounted LED grow light panel.
Cable Management: A critical consideration due to M2 and the LED panel moving with M1. A flexible drag chain or a carefully managed service loop is necessary to protect wiring.
The control software is written in C++ using the Arduino framework for the ESP32.
Development Approach: A spiral development model was used to incrementally build features:
Key Features:
Communication: USB Serial for commands and feedback.
The system is controlled via serial commands sent to the ESP32.
home 1
, home 2
, home_all
)A critical function to establish a known reference point (zero position) for each motor.
Direction (dir <motor> <0|1>
): Sets the intended physical direction of movement (0 for UP, 1 for DOWN). The software handles any necessary inversion of the DIR pin logic for specific motors.
Relative Moves (steps <motor> <count> <period_us>
): Moves the specified motor by a relative number of steps at a given step period.
Continuous Moves (speed <motor> <period_us>
): Runs the specified motor continuously at a given step period until a stop command.
Position Querying:
get_pos_steps <1|2>
: Returns the current position of the specified motor in microsteps from its home.get_pos_mm <1|2|all>
: Returns the current position of the motor(s) in millimeters from home.Absolute Panel Positioning (move_panel_to <target_abs_mm>
): This command moves the LED panel to a specific absolute vertical height from the base of the system.
The absolute panel position is calculated as: PanelPos_mm = (M1_currentPositionSteps / M1_steps_per_mm) + (M2_currentPositionSteps / M2_steps_per_mm)
The control logic determines the necessary movements for M1 and M2 to achieve this target, respecting individual motor software limits. Motors are moved sequentially.
Panel Position Query (get_panel_pos_mm
): (Should be get_panel_pos_mm
or similar, if get_pos_mm all
doesn't give the combined value). This command would report the calculated absolute height of the LED panel.
Steps per Millimeter (set_steps_per_mm <motor> <value>
): Allows calibration of how many microsteps correspond to 1mm of linear travel for each motor. This is crucial for accurate positioning in real-world units. Depends on lead screw pitch and microstepping settings.
Example Calculation: For a TR8*8 lead screw (8mm travel per revolution) and a 1.8° NEMA 17 motor (200 full steps/revolution) with 1/128 microstepping: Steps_per_mm = (200 full_steps/rev * 128 microsteps/full_step) / 8 mm/rev = 3200 microsteps/mm
Software End-Stops (Iteration 3): Constants like M1_MAX_TRAVEL_MM
and M2_MAX_TRAVEL_MM
define the maximum permissible travel for each motor from its home position. Movement commands check against these limits (if the motor is homed) to prevent over-travel and potential mechanical damage.
To prevent missed steps, reduce mechanical stress, and achieve smoother operation, especially when moving the significant mass of M2 and the LED panel, acceleration and deceleration (ramping) are implemented. This involves gradually changing the step period at the beginning and end of moves, rather than instantly starting/stopping at full speed.
(Describe your chosen method briefly: e.g., Linear Ramp or AccelStepper library integration).
(This is a section for you to personalize heavily! Here are some potential points you might have encountered or learned from):
Advanced Coordinated Motion: Implement more sophisticated algorithms for distributing movement between M1 and M2 when move_panel_to
is called (e.g., to minimize wear, maximize speed, or keep M2 in a preferred part of its range). Potentially simultaneous movement of M1 and M2 if using a library like AccelStepper.
The complete Arduino sketch for the ESP32 controller can be found here:[Link to your Git repository or code file]
(Embed key code snippets here if desired, e.g., the homeMotor
function or the move_panel_to
logic).
// Key code snippets can be embedded here.
// For example:
// void homeMotor(int motorNum) {
// // ... homing logic ...
// }
Visuals