August, 2025

EMS An Environment Management System

This project applies an Environmental Management System (EMS) concept as a Terrarium Management System designed specifically for a crested gecko habitat. The system regulates key environmental factors such as temperature and humidity, using an ultrasonic atomiser to maintain proper humidity levels and a set of fans to provide cooling when needed. Control and automation are managed through a NodeMCU v3.0 microcontroller paired with relays, while MQTT protocol handles network communications, enabling remote monitoring and control. The unit is powered by two 18650 lithium-ion batteries wired in parallel, providing a stable and efficient power supply for continuous habitat regulation.

EMS Wiring Diagram

Arduino Code for Node MCU

The code runs on an ESP8266 and uses a DHT11 sensor to measure temperature and humidity, displaying values on an OLED screen. It connects to Wi-Fi and communicates with an MQTT broker to publish sensor readings and receive commands. Relays control a fan and humidifier, which can run automatically based on sensor thresholds or manually through MQTT messages. The main loop cycles through reading the sensor, updating the display, publishing data, and managing MQTT connections.

/* * @file ESP8266 Environment Management System * @brief Client system for EMS * @author Zak G Rackham * Contact: zak.g.rackham@gmail.com */ #include #include #include #include #include #include #include #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels // Declaration for an SSD1306 display connected to I2C (SDA, SCL pins) // The pins for I2C are defined by the Wire-library. // On an arduino UNO: A4(SDA), A5(SCL) // On an arduino MEGA 2560: 20(SDA), 21(SCL) // On an arduino LEONARDO: 2(SDA), 3(SCL), ... #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) #define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); #define WIFI_NAME "SSID" #define WIFI_PASSWORD "PASSWORD" WiFiClient espClient; PubSubClient client; unsigned long lastMsg = 0; unsigned long lastMsg1 = 0; #define MSG_BUFFER_SIZE (50) char msg[MSG_BUFFER_SIZE]; char msg1[MSG_BUFFER_SIZE]; int value = 0; int temperature = 0; int humidity = 0; int count = 0; // Create an instance of the DHT11 class. // - For Arduino: Connect the sensor to Digital I/O Pin 2. // - For ESP32: Connect the sensor to pin GPIO2 or P2. // - For ESP8266: Connect the sensor to GPIO2 or D4. DHT11 dht11(2); int fansys = 14; bool fanstate = false; bool humidifierstate = false; void setup() { pinMode(2, OUTPUT); // Initialize the LED_BUILTIN pin as an output pinMode(14, OUTPUT); // Initialise the RELAY_TRIGGER pin as an output pinMode(0, OUTPUT); // Initialise the RELAY_TRIGGER pin as an output Serial.begin(9600); // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F("SSD1306 allocation failed")); } // Show initial display buffer contents on the screen -- // the library initializes this with an Adafruit splash screen. display.display(); delay(2000); // Pause for 2 seconds // Clear the buffer display.clearDisplay(); // Draw a single pixel in white display.drawPixel(10, 10, SSD1306_WHITE); // Show the display buffer on the screen. You MUST call display() after // drawing commands to make them visible on screen! display.display(); delay(2000); // display.display() is NOT necessary after every single drawing command, // unless that's what you want...rather, you can batch up a bunch of // drawing operations and then update the screen all at once by calling // display.display(). These examples demonstrate both approaches... //testdrawrect(); // Draw rectangles (outlines) WiFi.begin(WIFI_NAME, WIFI_PASSWORD); Serial.print("Connecting"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected, IP address: "); Serial.println(WiFi.localIP()); //Setup the pubsub client connection client.setClient(espClient); client.setServer(SERVER IP,1883); client.setCallback(callback); // client is now configured for use updatetext(); //Boot info screen } void callback(char* topic, byte* payload, unsigned int length) { Serial.print("Message arrived ["); Serial.print(topic); Serial.print("] "); for (int i = 0; i < length; i++) { Serial.print((char)payload[i]); } Serial.println(); // Switch on the LED if an 1 was received as first character if ((char)payload[0] == '1') { Serial.print("Message arrived = 1"); fanstate = true; } // Switch on the LED if an 1 was received as first character if ((char)payload[0] == '0') { Serial.print("Message arrived = 0"); fanstate = false; } if ((char)payload[0] == '2') { Serial.print("Message arrived = 2"); humidifierstate = true; } if ((char)payload[0] == '3') { Serial.print("Message arrived = 3"); humidifierstate = false; } } void reconnect() { // Loop until we're reconnected while (!client.connected()) { Serial.print("Attempting MQTT connection..."); // Create a random client ID String clientId = "ESP8266Client-"; clientId += String(random(0xffff), HEX); yield(); // Attempt to connect if (client.connect(clientId.c_str())) { Serial.println("connected"); yield(); // Once connected, publish an announcement... client.publish("outTopic", "EMS Online"); // ... and resubscribe client.subscribe("inTopic"); client.subscribe("emsSetFan", 0); client.subscribe("emsSetHumidifier", 0); } else { Serial.print("failed, rc="); Serial.print(client.state()); Serial.println(" try again on next execution"); // Wait 5 seconds before retrying delay(1000); break; } } } void testdrawrect(void) { display.clearDisplay(); for(int16_t i=0; i 2000) { lastMsg = now; ++value; snprintf (msg, MSG_BUFFER_SIZE, "%ld", temperature); Serial.print("Publish message: "); Serial.println(msg); client.publish("emsTemperature", msg); } } void publishhum() { unsigned long now1 = millis(); if (now1 - lastMsg1 > 2000) { lastMsg1 = now1; ++value; snprintf (msg1, MSG_BUFFER_SIZE, "%ld", humidity); Serial.print("Publish message: "); Serial.println(msg1); client.publish("emsHumidity", msg1); } } // the loop function runs over and over again forever void loop() { ++count; switch (count) { case 1: readdht(); updatedht11display(); break; case 2: client.loop(); yield(); break; case 3: publishtemp(); break; case 4: publishhum(); break; case 5: if (!client.connected()) { reconnect(); } count = 0; break; } if (!fanstate){ //Turn on cooling auto if (temperature > 25 && humidity > 50){ digitalWrite(14, HIGH); } else { digitalWrite(14, LOW); } } else { //Turn on cooling manual digitalWrite(14, HIGH); } if (!humidifierstate){ //Turn on cooling auto if (humidity < 50){ digitalWrite(0, HIGH); } else { digitalWrite(0, LOW); } } else { //Turn on cooling manual digitalWrite(0, HIGH); } yield(); }

Enclosure

The custom 3D-printed enclosure is designed in six sections to securely house the terrarium management electronics. One section contains two 18650 lithium-ion batteries wired in parallel, providing a combined capacity of around 5200 mAh at 3.7 V with a maximum continuous discharge of approximately 20 A, ensuring stable and reliable power delivery. Another section holds the OLED display and NodeMCU v3.0, allowing for both local monitoring and control. The remaining four sections are dedicated to housing the relays, keeping the layout compact, organized, and easy to service.