<body>
<h1>בקר החממה</h1>
<ul>
<li>טמפרטורה: <span id="temp" dir="ltr">24.6 °C</span></li>
<li>מאוורר: <span id="fanState">כבוי</span></li>
</ul>
<hr>
<button id="fan">הפעל מאוורר</button>
</body>
<!DOCTYPE html> <html lang="he" dir="rtl"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>בקר החממה</title> </head> <body> <h1>בקר החממה</h1> <p>טמפרטורה: <span id="temp" dir="ltr">24.6 °C</span></p> <p>מאוורר: <span id="fanState">כבוי</span></p> <button id="fan">הפעל מאוורר</button> </body> </html>
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>בקר החממה</title>
<style>
/* tag selectors: every element of that kind */
body {
font-family: Arial, sans-serif;
background: white;
color: #37474f;
}
h1 {
color: #2e7d32;
font-size: 24px;
}
span {
color: #1565c0;
font-weight: bold;
}
/* class selector: every element that carries class="card" */
.card {
box-sizing: border-box; /* width now includes padding and border */
width: 240px;
margin: 16px auto; /* auto on both sides centres the box */
padding: 12px;
border: 2px solid #2e7d32;
border-radius: 10px;
background: #f1f8e9;
text-align: center;
}
/* id selector: one element only, and it beats the span rule above */
#temp {
color: #c62828;
}
button {
background: #2e7d32;
color: white;
font-size: 16px;
padding: 10px 18px;
border: 2px solid #1b5e20;
border-radius: 8px;
}
/* applies only while the pointer is over the button */
button:hover {
background: #43a047;
}
</style>
</head>
<body>
<div class="card">
<h1>בקר החממה</h1>
<p>טמפרטורה: <span id="temp" dir="ltr">24.6 °C</span></p>
<p>מאוורר: <span id="fanState">כבוי</span></p>
<button id="fan">הפעל מאוורר</button>
</div>
</body>
</html>
<p>מאוורר: <span id="fanState">כבוי</span></p>
<p>לחיצות: <span id="count">0</span></p>
<button id="fan">הפעל מאוורר</button>
<script>
let fanOn = false, clicks = 0;
const ON = 'פועל', OFF = 'כבוי';
const state = document.getElementById('fanState');
const count = document.getElementById('count');
document.getElementById('fan').addEventListener('click', function () {
fanOn = !fanOn;
clicks = clicks + 1;
state.textContent = fanOn ? ON : OFF;
count.textContent = clicks;
});
</script>
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>בקר החממה</title>
<style>
body { font-family: Arial, sans-serif; background: white; color: #263238; }
.card { box-sizing: border-box; width: 260px; margin: 16px auto; padding: 12px;
border: 2px solid #2e7d32; border-radius: 10px; background: #f1f8e9;
text-align: center; }
h1 { color: #2e7d32; font-size: 24px; }
span { color: #1565c0; font-weight: bold; }
button { background: #2e7d32; color: white; font-size: 16px; padding: 10px 18px;
border: 2px solid #1b5e20; border-radius: 8px; }
button:disabled { background: #9e9e9e; border-color: #757575; }
/* the class that the script adds and removes */
.on { color: #2e7d32; }
</style>
</head>
<body>
<div class="card">
<h1>בקר החממה</h1>
<p>טמפרטורה: <span id="temp" dir="ltr">24.6 °C</span></p>
<p>ערך רצוי: <span id="setOut" dir="ltr">25 °C</span></p>
<input type="range" id="set" min="20" max="30" step="0.5" value="25">
<p><label><input type="checkbox" id="auto"> בקרה אוטומטית</label></p>
<p>מאוורר: <span id="fanState">כבוי</span></p>
<button id="fan">הפעל מאוורר</button>
</div>
<script>
// state: lives in the page and starts again on every refresh
let fanOn = false;
let ticks = 0;
let temp = 24.6;
// the words the page shows
const ON = 'פועל', OFF = 'כבוי';
const START = 'הפעל מאוורר', STOP = 'כבה מאוורר';
// the elements, found once by id
const tempOut = document.getElementById('temp');
const slider = document.getElementById('set');
const setOut = document.getElementById('setOut');
const autoBox = document.getElementById('auto');
const fanState = document.getElementById('fanState');
const fanBtn = document.getElementById('fan');
// the only function that writes to the page
function show() {
tempOut.textContent = temp.toFixed(1) + ' °C';
setOut.textContent = slider.value + ' °C';
fanState.textContent = fanOn ? ON : OFF;
fanState.classList.toggle('on', fanOn);
fanBtn.textContent = fanOn ? STOP : START;
fanBtn.disabled = autoBox.checked;
}
// automatic mode: a threshold with a band of 1 degree on each side
function decide() {
if (autoBox.checked) {
const set = Number(slider.value);
if (temp > set + 1) fanOn = true;
if (temp < set - 1) fanOn = false;
}
show();
}
// events: the browser calls these functions, the page never waits in a loop
fanBtn.addEventListener('click', function () {
fanOn = !fanOn;
show();
});
slider.addEventListener('input', decide);
autoBox.addEventListener('change', decide);
// timer: once a second an invented reading, a slow wave around 24.6,
// computed in tenths of a degree and rounded
setInterval(function () {
ticks = ticks + 1;
temp = Math.round(246 + 24 * Math.sin(2 * Math.PI * ticks / 40)) / 10;
decide();
}, 1000);
show();
</script>
</body>
</html>
scheme: httphost: 192.168.1.50port: 80path: /fanquery: state=on
server.on("/fan", handleFan);server.arg("state") == "on"
HTTP/1.1 200 OK
Content-Type: text/plain
on
// 4 The greenhouse controller as a web server.
// The browser asks and the ESP32 answers:
// / returns the page
// /fan?state=on switches the fan (or state=off) and answers with a short text
// /data returns the reading as JSON
// Any other path gets the status code 404.
#include <WiFi.h>
#include <WebServer.h>
const char* WIFI_NAME = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_PASSWORD";
const int PIN_SENSOR = 34; // LM35 output, 10 mV per degC
const int PIN_FAN = 26; // gate of the MOSFET that switches the fan
const int PIN_LED = 2; // built-in LED, mirrors the fan
WebServer server(80); // HTTP listens on port 80
bool fanOn = false;
// The whole page, kept in flash as one raw string literal.
const char PAGE[] = R"rawliteral(
<!DOCTYPE html>
<html lang="he" dir="rtl">
<head><meta charset="utf-8"><title>בקר החממה</title>
<meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body style="font-family: sans-serif; background: white">
<h1>בקר החממה</h1>
<p>טמפרטורה: <span id="temp" dir="ltr">-- °C</span></p>
<p>מאוורר: <span id="fanState">כבוי</span></p>
<button id="fan">הפעל מאוורר</button>
<script>
function el(id) { return document.getElementById(id); }
var next = "on";
var STATE = { on: "פועל", off: "כבוי" };
var BUTTON = { on: "כבה מאוורר", off: "הפעל מאוורר" };
function show(d) {
next = (d.fan == "on") ? "off" : "on";
el("temp").textContent = d.temp.toFixed(1) + " °C";
el("fanState").textContent = STATE[d.fan];
el("fan").textContent = BUTTON[d.fan];
}
function lost() { el("fanState").textContent = "אין חיבור לבקר"; }
function refresh() { fetch("/data").then(r => r.json()).then(show).catch(lost); }
el("fan").onclick = function () { fetch("/fan?state=" + next).then(refresh).catch(lost); };
refresh(); setInterval(refresh, 2000);
</script>
</body></html>
)rawliteral";
float readTemperature() {
return analogReadMilliVolts(PIN_SENSOR) / 10.0;
}
void setFan(bool on) {
fanOn = on;
digitalWrite(PIN_FAN, on ? HIGH : LOW);
digitalWrite(PIN_LED, on ? HIGH : LOW);
}
// GET / : the page itself
void handleRoot() {
server.send(200, "text/html", PAGE);
}
// GET /fan?state=on or GET /fan?state=off : switch, then answer with the new state
void handleFan() {
String state = server.arg("state");
if (state == "on") setFan(true);
if (state == "off") setFan(false);
server.send(200, "text/plain", fanOn ? "on" : "off");
}
// GET /data : {"temp": 24.6, "fan": "on"}
void handleData() {
String json = "{\"temp\": ";
json += String(readTemperature(), 1);
json += ", \"fan\": \"";
json += fanOn ? "on" : "off";
json += "\"}";
server.send(200, "application/json", json);
}
// Any other path : 404
void handleNotFound() {
server.send(404, "text/plain", "Not Found");
}
void setup() {
Serial.begin(115200);
pinMode(PIN_FAN, OUTPUT);
pinMode(PIN_LED, OUTPUT);
setFan(false);
WiFi.begin(WIFI_NAME, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.print("Type this address in the browser: http://");
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/fan", handleFan);
server.on("/data", handleData);
server.onNotFound(handleNotFound);
server.begin();
}
void loop() {
server.handleClient(); // answers one waiting request, if there is one
}
GET /data HTTP/1.1 Host: 192.168.1.50
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 27
{"temp": 24.6, "fan": "on"}
// MQTT 3.1.1 - the dialogue of the greenhouse, written out as text. // A reference to read, not a program to run. // Broker: 192.168.1.10, TCP port 1883. // C = controller gh-esp32 B = broker P = phone dashboard C -> B CONNECT client id "gh-esp32", clean session 22 bytes B -> C CONNACK connection accepted 4 bytes C -> B SUBSCRIBE "greenhouse/fan/set" B -> C SUBACK subscription accepted P -> B SUBSCRIBE "greenhouse/#" B -> P SUBACK subscription accepted C -> B PUBLISH "greenhouse/temp" payload 24.6 23 bytes B -> P PUBLISH "greenhouse/temp" payload 24.6 23 bytes P -> B PUBLISH "greenhouse/fan/set" payload on 24 bytes B -> C PUBLISH "greenhouse/fan/set" payload on 24 bytes B -> P PUBLISH "greenhouse/fan/set" payload on 24 bytes C -> B PUBLISH "greenhouse/fan/state" payload on 26 bytes B -> P PUBLISH "greenhouse/fan/state" payload on 26 bytes C -> B PINGREQ nothing to send, the link stays alive 2 bytes B -> C PINGRESP 2 bytes
// 6 Greenhouse controller on MQTT: last will, retained state, JSON.
// Library needed: PubSubClient (Arduino IDE, Library Manager).
// The broker runs on the lab computer; the ESP32 only knows its address.
#include <WiFi.h>
#include <PubSubClient.h>
const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_PASSWORD";
const char* BROKER = "192.168.1.10";
const int BROKER_PORT = 1883;
const int PIN_LM35 = 34; // ADC1 input, LM35 gives 10 mV per degC
const int PIN_FAN = 26; // gate of the logic-level MOSFET
const int PIN_LED = 2; // built-in LED mirrors the fan
WiFiClient net;
PubSubClient mqtt(net);
bool fanOn = false;
unsigned long lastTry = 0; // last connection attempt [ms]
unsigned long lastRead = 0; // last temperature publish [ms]
// Retained: a dashboard that opens later gets the state at once.
void publishFanState() {
mqtt.publish("greenhouse/fan/state", fanOn ? "on" : "off", true);
}
void setFan(bool on) {
fanOn = on;
digitalWrite(PIN_FAN, on ? HIGH : LOW);
digitalWrite(PIN_LED, on ? HIGH : LOW);
publishFanState();
}
// The payload is NOT zero-terminated, so strcmp() on it would read past
// the end. Compare the length first, then the bytes.
void onMessage(char* topic, byte* payload, unsigned int length) {
if (strcmp(topic, "greenhouse/fan/set") != 0) return;
if (length == 2 && memcmp(payload, "on", 2) == 0) setFan(true);
if (length == 3 && memcmp(payload, "off", 3) == 0) setFan(false);
}
// One attempt every 2 s. It returns at once, so loop() never stalls.
void reconnect() {
if (WiFi.status() != WL_CONNECTED) return;
if (millis() - lastTry < 2000) return;
lastTry = millis();
// connect(client id, will topic, will QoS, will retain, will message)
if (mqtt.connect("gh-esp32", "greenhouse/status", 1, true, "offline")) {
mqtt.publish("greenhouse/status", "online", true);
mqtt.subscribe("greenhouse/fan/set");
publishFanState();
Serial.println("MQTT connected");
} else {
Serial.print("MQTT failed, state ");
Serial.println(mqtt.state());
}
}
void setup() {
Serial.begin(115200);
pinMode(PIN_FAN, OUTPUT);
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_FAN, LOW);
digitalWrite(PIN_LED, LOW);
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS); // connects in the background
mqtt.setServer(BROKER, BROKER_PORT);
mqtt.setCallback(onMessage);
}
void loop() {
if (!mqtt.connected()) reconnect();
mqtt.loop(); // receives messages and keeps the connection alive
if (mqtt.connected() && millis() - lastRead >= 5000) {
lastRead = millis();
float tempC = analogReadMilliVolts(PIN_LM35) / 10.0;
char text[8];
dtostrf(tempC, 1, 1, text); // "24.6"
mqtt.publish("greenhouse/temp", text); // QoS 0, not retained
char json[48];
snprintf(json, sizeof(json), "{\"temp\":%.1f,\"fan\":\"%s\"}",
tempC, fanOn ? "on" : "off");
mqtt.publish("greenhouse/data", json); // {"temp":24.6,"fan":"on"}
}
}
{"temp":24.6,"fan":"on"}