Live Demo
This is the real control code running in your browser. The greenhouse itself is simulated: the physics model stands in for the hardware, so the sensors, controller, model and security code all behave as they would on a microcontroller. One real second is ten simulated minutes at 1× speed.
tech with the password Gr0w-Safe!, then try saving a low mark of <script> or 70.All systems normal
Actuators Mode: Automatic
Last 24 hours Temperature Soil moisture Target band
Watering forecast (machine learning)
- Model
- Collecting data…
- Samples
- 0
- R²
- —
- MSE
- —
- Next watering
- —
Access Guest: view only
Scenario
Data log
Search the log by time, or sort it.
Event log
The Brief
The school's agriculture class grows seedlings in a small greenhouse. Plants die over long weekends and holidays because nobody is there to water them, open the vents on hot days or switch on the grow lamp in winter. Students also want to see what is happening inside the greenhouse from a classroom computer.
Stakeholders
- Agriculture teacher: the client. Needs healthy plants and a record of conditions for lessons.
- Students: view the dashboard, but must not be able to change settings.
- Technician: sets targets, runs the system by hand for maintenance and refills the tank.
Constraints
- Hardware budget under $150: a microcontroller with Wi-Fi, a capacitive soil moisture sensor, a temperature and humidity sensor, a light sensor, a relay board, a 12 V pump, a fan and an LED grow lamp.
- Must fail safe: a broken sensor or an empty tank must never flood the beds or burn out the pump.
- Ten weeks of development time, working alone.
Requirements
Each requirement is written so it can be tested. The last column names the test that proves it, so the evaluation at the end can point to evidence rather than opinion.
| ID | Functional requirement | Evidence |
|---|---|---|
| F1 | Water the beds when soil moisture falls below the low mark and stop at the high mark. | T3, T4, T5 |
| F2 | Run the fan when the temperature rises above the maximum, and stop 3 °C below it. | Live demo: try a heatwave |
| F3 | Run the grow lamp during growing hours (06:00–20:00) when light is low. | Live demo: try an overcast day |
| F4 | Detect an out-of-range sensor reading, stop watering and raise an alert. | T1, T2, T6 |
| F5 | Log readings every 10 minutes for 7 days; search by time, list the hottest readings and export CSV. | T9, T10, T11 |
| F6 | Predict when the next watering is due from the conditions so far. | T8 |
| F7 | Only a signed-in technician can change setpoints, switch to manual mode or refill the tank. | T12–T17 |
| ID | Non-functional requirement | Evidence |
|---|---|---|
| N1 | Dashboard updates at least once a second. | Live demo refreshes 10 times a second |
| N2 | Passwords are never stored or logged in plain text. | Salted SHA-256 hashes; see the sign-in code |
| N3 | Sign-in locks for 20 seconds after three wrong attempts. | T16, T17 |
| N4 | Usable by keyboard and screen reader; works on a phone. | Labelled controls, live-region log, responsive layout |
| N5 | Every setting change is recorded with who made it. | Event log (AUTH and CTRL entries) |
Where Each Topic Appears
A strong major project draws on more than one focus area. This one uses all seven. Each card names what the project borrows from that topic and where to see it in the demo.
Sensor base class with four subclasses that each override measure(), and a Greenhouse object composed of the rest.See: Class design
Programming MechatronicsSensors, actuators and a closed-loop control algorithm with hysteresis, fault detection and a fail-safe state.See: Actuators, Scenario
Secure Software ArchitectureSalted password hashes, a lockout against brute force, role-based access, server-side style checks and an audit trail.See: Access, Event log
Programming for the WebA responsive dashboard that updates live, draws charts on a canvas and exports data, with output written safely as text.See: the whole dashboard
Software AutomationA linear regression model trained continuously on sensor data to predict evaporation and forecast the next watering.See: Watering forecast
Software Engineering ProjectRequirements tied to tests, a data dictionary, a test table and an evaluation against the original brief.See: this page's structure
Class Design
Each class has one job. The controller only decides: it returns the actuator states it wants and never switches hardware itself. That separation is what makes it possible to unit test the control logic without a greenhouse.
| Class | Responsibility | Key methods | Relationship |
|---|---|---|---|
Sensor | Take a reading and check it is physically possible | read(), measure(), inRange() | Abstract parent |
TemperatureSensor, MoistureSensor, LightSensor, HumiditySensor | Measure one quantity | measure() (overridden) | Inherit from Sensor |
Actuator | Switch a device and track its energy use | set(), energyWh() | Used by Greenhouse |
Controller | Turn readings into decisions; validate setpoints | decide(), validateSetpoints() | Used by Greenhouse |
DataLogger | Keep 7 days of records; search, sort and export them | findByTime(), mergeSort(), toCSV() | Used by Greenhouse |
LinearRegression | Learn evaporation rate from temperature | add(), fit(), predict() | Used by Greenhouse |
AuthService | Store salted hashes, check sign-ins, enforce lockout | addUser(), login() | Used by the dashboard |
Greenhouse | Run one minute: sense, decide, act, record, learn | tick(), forecastWatering() | Composed of all of the above |
class Sensor:
def __init__(self, name, minimum, maximum):
self.name = name
self.minimum = minimum
self.maximum = maximum
self.valid = False
def measure(self):
raise NotImplementedError # each subclass reads its own hardware
def read(self):
value = self.measure()
self.valid = self.minimum <= value <= self.maximum
return value
class MoistureSensor(Sensor):
def __init__(self, pin):
super().__init__("Soil moisture", 0, 100)
self.pin = pin
def measure(self):
raw = self.pin.read_u16() # 0–65535 from the ADC
return (52000 - raw) / 300 # calibrated: dry air 0 %, water 100 %
The demo runs the same design in JavaScript so it can run in a browser. On the real hardware, MicroPython runs the Python version above.
Key Algorithms
Control loop with hysteresis
Switching the pump on below 35% and off at the same value would make it chatter on and off as the reading wobbles. Using two marks, a low one to start and a high one to stop, gives a dead band in between where nothing changes.
BEGIN ControlCycle
READ moisture, temperature, light
IF moisture reading is outside 0–100 THEN
pump ← OFF
RAISE ALERT "Moisture sensor fault"
ELSE
IF tank level = 0 THEN
pump ← OFF
RAISE ALERT "Tank empty"
ELSE
IF moisture < lowMark THEN
pump ← ON
ELSE
IF moisture ≥ highMark THEN
pump ← OFF
ENDIF
ENDIF
ENDIF
ENDIF
IF temperature > maxTemp THEN
fan ← ON
ELSE
IF temperature < maxTemp − 3 THEN
fan ← OFF
ENDIF
ENDIF
IF hour is between 06:00 and 20:00 AND light < 250 THEN
lamp ← ON
ELSE
IF hour is outside 06:00–20:00 OR light > 700 THEN
lamp ← OFF
ENDIF
ENDIF
WRITE log record every 10 minutes
END ControlCycle
Learning the evaporation rate
Soil dries faster when it is hot. While the pump is off, the system measures how fast moisture falls over each 10 minutes and pairs it with the temperature at the time. A least-squares line through those points predicts the drying rate at any temperature, which gives the time until the next watering. This is supervised learning: every sample has a known answer, the measured rate.
def fit_line(xs, ys):
"""Return slope a, intercept b and R² for y = a*x + b."""
n = len(xs)
mean_x = sum(xs) / n
mean_y = sum(ys) / n
sxy = sum((x - mean_x) * (y - mean_y) for x, y in zip(xs, ys))
sxx = sum((x - mean_x) ** 2 for x in xs)
a = sxy / sxx
b = mean_y - a * mean_x
ss_res = sum((y - (a * x + b)) ** 2 for x, y in zip(xs, ys))
ss_tot = sum((y - mean_y) ** 2 for y in ys)
return a, b, 1 - ss_res / ss_tot
def minutes_until_watering(moisture, low_mark, temperature, a, b):
rate = a * temperature + b # predicted % lost per minute
return (moisture - low_mark) / rate
Signing in safely
The system stores a random salt and the SHA-256 hash of salt plus password, never the password. Wrong usernames and wrong passwords get the same message, so an attacker can't learn which accounts exist, and three failures lock sign-in for 20 seconds.
import hashlib, secrets, time
users = {} # username -> (salt, hash, role)
failures, locked_until = 0, 0
def add_user(username, password, role):
salt = secrets.token_hex(8)
digest = hashlib.sha256((salt + password).encode()).hexdigest()
users[username] = (salt, digest, role)
def login(username, password):
global failures, locked_until
if time.time() < locked_until:
return None, "Too many attempts. Try again soon."
salt, digest, role = users.get(username, ("x", None, None))
if hashlib.sha256((salt + password).encode()).hexdigest() == digest:
failures = 0
return role, "Signed in"
failures += 1
if failures >= 3:
failures, locked_until = 0, time.time() + 20
return None, "Incorrect username or password."
Data Dictionary
One record is written to the log every 10 simulated minutes. The log holds 1,008 records (7 days); the oldest record is dropped when a new one arrives. Records stay in time order, which is what makes binary search possible.
| Field | Data type | Format / unit | Validation | Example |
|---|---|---|---|---|
time | Integer | Minutes since start | Multiple of 10, increasing | 1560 (Day 2 02:00) |
temp | Real | °C, 1 decimal place | −10 to 60, else sensor fault | 24.6 |
moisture | Real | % volumetric | 0 to 100, else sensor fault | 41.0 |
light | Real | lux | 0 to 2000 | 612 |
humidity | Real | % relative | 0 to 100 | 58 |
pump, fan, lamp | Boolean | 1 = on, 0 = off | 0 or 1 | 1 |
Test Results
These unit tests run the real classes in your browser, separately from the live demo. Each uses a chosen category of test data: normal data the system should accept, boundary data at the exact edge of a range, and erroneous data it must reject.
| Test | Description | Test data | Expected | Actual | Result |
|---|---|---|---|---|---|
| Press “Run the tests” to fill this table. | |||||
Evaluation
The evaluation goes back to the brief and the requirements, and judges each one against evidence.
| Requirement | Met? | Evidence and comment |
|---|---|---|
| F1 Watering | Yes | T3–T5 pass. The trend chart shows moisture held inside the target band across day and night. |
| F2 Venting | Yes | In a heatwave the fan runs through the afternoon, but the temperature can still pass the maximum: the fan alone is not enough on the hottest days. |
| F4 Fault handling | Yes | T6 passes; breaking the sensor stops watering and raises an alert within one cycle. |
| F6 Forecast | Partly | R² settles between about 0.75 and 0.8 after a day of data. The model uses temperature only, and sunshine raises both temperature and drying, so the slope comes out slightly high and forecasts are least accurate at dawn and on overcast days. |
| F7, N2, N3 Security | Yes | T12–T17 pass. Passwords are hashed with a salt; lockout works. See the note above on production hashing. |
| N4 Accessibility | Yes | All controls are buttons or labelled inputs, alerts use a live region, and the layout stacks on a phone. |
Improvements for the next version
- Add light as a second feature in the model (multiple linear regression) to improve dawn forecasts.
- Add shade cloth on a servo for heatwaves, since venting alone can't hold the maximum.
- Send a phone notification when the tank is low, rather than only showing an alert.
- Replace SHA-256 with a password-hashing algorithm such as bcrypt when moving to a real server.