Community project

Automated Aerated Composting

Photo of Automated Aerated Composting
Generated with AI

Kevin Butay

Published November 12, 2026

This project automates the aeration and mixing of a compost bin using an ESP32 microcontroller to monitor temperature and moisture while controlling a blower fan and mixing motor. The system tracks decomposition progress over time and can send status updates via GSM/GPRS, making it possible to manage compost from anywhere.

Built from readily available components including temperature and soil moisture sensors, 12V motors with a geared reducer and centrifugal blower, dual buck converters for power management, and a SIM800L module for remote communication, this guide provides a complete wiring diagram, parts list, firmware with calibration instructions, and step-by-step assembly to get a fully functional automated composter running.

Wiring diagram

Wiring diagram for Automated Aerated Composting
Loading diagram…

Gather all the parts

QtyComponent
1

DS18B20

Digital temperature sensor using OneWire protocol

1

4.7k Pull Up Resistor

4.7kΩ pull-up resistor used in 1-Wire (OneWire) communication buses. Essential passive component that pulls the single data line to 3.3V or 5V when idle. Works with 1-Wire protocol devices (DS18B20, DS18S20, iButton, etc.) to ensure proper bus communication at standard 16.3 kbit/s data rates. Supports both 3.3V and 5V systems.

1

Capacitive Soil Moisture Sensor v1.2

The coated probe that measures how wet the compost is without exposed metal electrodes.

1

Ceramic Capacitor

100 nF, 50 V

Through-hole ceramic capacitor bought by value, such as 22 pF, 10 nF or 100 nF (0.1 µF). It is non-polarised, so either lead can go to either net. Its most common job is decoupling: a 100 nF capacitor between an IC's supply pin and ground, placed as close to the chip as possible.

1

SIM800L GSM/GPRS Module

Compact GSM/GPRS module based on SIMCom SIM800L chip. Features a built-in SIM card slot and antenna connector. Communicates via UART using AT commands. Operates at 3.4V–4.4V supply (must NOT be powered from 3.3V LDO; use a boost converter such as MT3608). Peak current draw up to 2A during GSM TX bursts — requires external dedicated power supply with bulk decoupling capacitor (100–470uF). UART logic is nominally 2.8V; when driven by ESP32 (3.3V), a resistor voltage divider on the ESP32 TX → SIM800L RX line is required to shift 3.3V down to ~2.8V. Built-in SIM card slot and antenna port are physical features of the module PCB.

1

Electrolytic Capacitor

2200 µF low-ESR, 10 V or higher

Radial aluminium electrolytic capacitor bought by value and voltage rating, such as 10 µF, 100 µF or 1000 µF 10 V. It is polarised: the + lead goes to the higher voltage and the - lead, marked by the stripe on the sleeve, to the lower voltage or ground. It is mostly used for bulk filtering across a power rail, with a voltage rating comfortably above the rail.

1

Resistor

10 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

56 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

LM2596 adjustable buck converter

Adjusted to 3.3 V for relay board

A small converter that reduces the 12 V supply to a stable 3.3 V supply for the relay board.

1

24v Buck Converter

Adjusted to 4.0 V, 2 A minimum

LM2596-based adjustable step-down buck converter module. Commonly used to regulate a higher battery rail, such as a 2S 18650 pack, down to 5V for Arduino logic. It is a regulator, not a charger or battery protection board.

1

JGA25-370 12 V 12 RPM geared motor

The slow, high-torque motor that turns the compost shaft and staggered mixing blades.

1

1N4001 Flyback Diode

General-purpose silicon rectifier diode used as a flyback / freewheeling diode to suppress back-EMF voltage spikes from inductive loads (DC motors, relays). Rated 50 V reverse voltage and 1 A forward current. Connected in parallel across the motor terminals with cathode (striped band) to the motor positive (+) / 5 V supply side and anode to the motor negative (−) / transistor collector side. Requires no firmware or library — purely passive protection component. Higher-voltage 1N400x rectifier variants are compatible substitutes when the current and package fit the design.

1

9733 12 V centrifugal turbo blower with speed controller

The high-speed centrifugal fan that pulses air through the bottom PVC manifold.

1

12 V 4 A regulated wall adapter

The mains-isolated plug-in supply that provides the reactor's 12 volt power rail.

1

5 A inline blade-fuse holder with 5 A fuse

The replaceable safety fuse placed at the 12 volt adapter output to protect the wiring from a short circuit.

1

3.3 V-input two-channel optocoupler relay module

A compact two-switch board that lets the ESP32 safely turn the 12 V blower and mixer on and off.

1

Ceramic Capacitor

100 nF, 50 V

Through-hole ceramic capacitor bought by value, such as 22 pF, 10 nF or 100 nF (0.1 µF). It is non-polarised, so either lead can go to either net. Its most common job is decoupling: a 100 nF capacitor between an IC's supply pin and ground, placed as close to the chip as possible.

1

Resistor

10 kΩ

Through-hole resistor (current-limiting in series with an LED)

1

Resistor

10 kΩ

Through-hole resistor (current-limiting in series with an LED)

Assemble it in 8 steps

1. Keep the moving parts outside the breadboard

Use a 50 L vessel. Build the false floor, air manifold, blower, lid motor, shaft, bearing, and blades as before. Keep every moving part and all 12 V wires outside the dry electronics box; a solderless breadboard is only for the small controller wires.

  • Leave at least 50 mm below the mesh for the air chamber.
  • Test that the empty blade stack turns without touching the vessel.
  • A breadboard must never carry blower or motor current; its thin metal clips can heat up, melt plastic, or start a fire.

2. Position the one breadboard

Use one full-size 830-tie-point breadboard. Straddle the ESP32 across the center groove with its USB socket facing an end. Use the left power rails as 3V3 and GND for the sensors. Place the SIM800L at the far end, then place the 4.7 kΩ, 10 kΩ, and 56 kΩ resistors plus the 100 nF capacitor in the remaining center rows. Keep the 2200 µF low-ESR capacitor beside the SIM800L but connect it directly to the GSM buck converter with short wires.

  • Leave the right-side rails unused so the crowded signal wires stay easy to trace.
  • The ESP32, SIM800L, resistors, and small capacitor fit on the breadboard; the probes connect with jumper leads from the vessel.
  • Do not put either buck converter, the relay board, fuse, motor, or blower into breadboard holes; they are connected beside it with screw terminals and properly sized wire.

3. Wire the breadboard signal section

Connect DS18B20 VCC → ESP32 3V3 (power), GND → ESP32 GND (ground), and DATA → GPIO4 (temperature signal). Put the 4.7 kΩ resistor between the same GPIO4 row and 3V3. Connect moisture VCC → 3V3 (power), GND → GND (ground), and AOUT → GPIO33 (moisture signal); place the 100 nF capacitor from AOUT to GND. Connect SIM800L TX → GPIO16 (data). Connect GPIO17 through 10 kΩ to SIM800L RX, then connect 56 kΩ from SIM800L RX to GND; this makes the ESP32 signal safe for the GSM input.

  • Use short jumpers for the SIM800L connections and keep its antenna outside the metal electronics box.
  • All grounds, including both buck converters, ESP32, sensors, GSM module, and relay board, must join together.
  • Never connect GPIO17 directly to SIM800L RX; the resistor pair prevents excessive voltage at that input.

4. Set and mount the two power converters

With the electronics disconnected, connect the 12 V adapter through the 5 A fuse. Set logic_buck to exactly 3.3 V for the relay board. Set gsm_buck to exactly 4.0 V for the SIM800L. Mount both converters on a non-conductive panel next to the breadboard and connect the 2200 µF low-ESR capacitor directly across the GSM converter output: + to VOUT+ and the striped - lead to VOUT-.

  • Measure each output with a multimeter before attaching it to any module.
  • Power the ESP32 from its USB socket while programming; do not feed the relay supply into its 3V3 pin.
  • A converter set too high can permanently damage the SIM800L or relay board. Reversing the capacitor can make it burst.

5. Wire the relay board beside the breadboard

Mount the 3.3 V two-channel relay module beside, not on, the breadboard. Connect relay DC+ → the 3.3 V converter output (power), DC- → common GND (ground), IN1 → GPIO25 (blower control), and IN2 → GPIO26 (mixer control). Select high-level triggering on the relay board. Put the fused 12 V positive wire into both COM terminals. Connect NO1 → blower speed-controller positive input (switched blower power) and NO2 → mixer motor M+ (switched mixer power). Connect blower GND and motor M- directly to the 12 V return. Put the flyback diode across the motor: striped end to M+ and unstriped end to M-.

  • Use screw terminals and wire rated for the motor's starting current.
  • With the vessel empty, first test one relay channel at a time.
  • Keep the bare 12 V screw terminals covered. A loose high-current wire can overheat, short the adapter, or start the motor unexpectedly.

6. Mount the dry controller box

Secure the breadboard, relay board, converters, fuse, and SIM800L in a dry box outside the compost vessel. Bring only the sensor cables and the motor/blower cables through strain-relieved holes. Plug the ESP32 into USB, connect the 12 V adapter, then use Deploy to load the controller.

  • Firmware uses 10 minutes aeration every 3 hours, thermal ON at 60 C and release at 58 C, and 2 minutes mixing every 6 hours.
  • Calibrate moisture endpoints in the actual substrate and set MOISTURE_CALIBRATED true before using 45 to 70 percent alerts.
  • The five-day cool-temperature indicator resets after any power interruption and is not proof of maturity.
  • Serial CSV is not persistent data logging; capture it externally throughout the trial.
  • Validate motor starting and stall current for the larger vessel before loading; the existing motor and adapter are not certified adequate for 50 L.
  • Label the 3.3 V relay supply, 4.0 V GSM supply, and 12 V motor wires so they cannot be mixed later.
  • Insert an activated SIM card before powering the SIM800L.
  • Unplug USB and the 12 V adapter before moving any jumper or screw-terminal wire; moisture near the controller can cause shorts and corrosion.

7. Add temperature supply decoupling

Place the additional 100 nF capacitor between DS18B20 VCC and GND close to the sensor connection. Retain the separate moisture signal filter.

  • Keep the existing 3.3 V relay converter setting. This design still powers the ESP32 by USB; do not change that converter to 5 V with the current relay attached.
  • Keep hands out of the vessel while powered. Relays and firmware are not emergency stops or independent overtemperature protection.

8. Add reset protection and capture uptime records

Connect each 10 kohm pulldown from its relay input to GND. Use high-trigger relays only. No RTC or microSD is used for this bench-testing build; GPIO21 and GPIO22 are unused.

  • RECORD lines print over USB every five minutes and at actuator changes. They contain uptime and batch ID, not calendar time. Capture them externally if needed; no historical database is stored onboard.
  • A restart preserves the batch identifier but resets schedules and the consecutive monitored-temperature period. Uptime wraps after about 49.7 days; use shorter bench runs and mark every reboot in external records.
  • Test reset behavior with motor and blower disconnected. Pulldowns only suit the existing high-trigger configuration.
  • Motor protection and supply ratings remain unverified for stall current. Do not run a loaded reactor until these are validated.

Review all connections

1. Connections between "temp_probe" and "ESP32"

Functiontemp_probeESP32
powerVCC → sensor power3V3
groundGND → ground returnGND
dataDATA → core-temperature dataGPIO 4

2. Connections between "onewire_pullup" and "ESP32"

Functiononewire_pullupESP32
powerVCC → pull-up supply3V3
groundGND → module ground referenceGND
dataDATA_LINE → DS18B20 DATAEXT

3. Connections between "moisture_probe" and "ESP32"

Functionmoisture_probeESP32
powerVCC → sensor power3V3
groundGND → ground returnGND
analogAOUT → moisture signalGPIO 33

4. Connections between "moisture_filter" and "ESP32"

Functionmoisture_filterESP32
ground2 → noise filter returnGND
analog1 → Capacitive Soil Moisture Sensor v1.2 AOUTEXT

5. Connections between "gsm" and "ESP32"

FunctiongsmESP32
powerVCC → 24v Buck Converter VOUT+EXT
groundGND → 24v Buck Converter VOUT-EXT
uartTX → GSM-to-ESP32 receive dataGPIO 16

6. Connections between "gsm_bulk_cap" and "ESP32"

Functiongsm_bulk_capESP32
power+ → 24v Buck Converter VOUT+EXT
ground- → 24v Buck Converter VOUT-EXT

7. Connections between "gsm_tx_divider_top" and "ESP32"

Functiongsm_tx_divider_topESP32
digitalP1 → ESP32 UART transmitGPIO 17
digitalP2 → SIM800L GSM/GPRS Module RXEXT

8. Connections between "gsm_tx_divider_bottom" and "ESP32"

Functiongsm_tx_divider_bottomESP32
digitalP1 → SIM800L GSM/GPRS Module RXEXT
groundP2 → divider ground returnGND

9. Connections between "power_adapter" and "ESP32"

Functionpower_adapterESP32
power+12V → 5 A inline blade-fuse holder with 5 A fuse INEXT
groundGND → shared 12 V returnGND

10. Connections between "logic_buck" and "ESP32"

Functionlogic_buckESP32
groundVIN- → 12 V supply returnGND
powerVOUT+ → 3.3 V-input two-channel optocoupler relay module DC+EXT
groundVOUT- → 3.3 V-input two-channel optocoupler relay module DC-EXT
powerVIN+ → 5 A inline blade-fuse holder with 5 A fuse OUTEXT

11. Connections between "gsm_buck" and "ESP32"

Functiongsm_buckESP32
groundVIN- → 12 V supply returnGND
powerVIN+ → 5 A inline blade-fuse holder with 5 A fuse OUTEXT

12. Connections between "mixer_motor" and "ESP32"

Functionmixer_motorESP32
groundM- → motor returnGND

13. Connections between "motor_flyback_diode" and "ESP32"

Functionmotor_flyback_diodeESP32
passiveCathode → JGA25-370 12 V 12 RPM geared motor M+EXT
passiveAnode → JGA25-370 12 V 12 RPM geared motor M-EXT

14. Connections between "blower" and "ESP32"

FunctionblowerESP32
groundGND → blower returnGND

15. Connections between "dual_relay" and "ESP32"

Functiondual_relayESP32
digitalIN1 → blower control signalGPIO 25
digitalIN2 → mixer control signalGPIO 26
powerNO1 → 9733 12 V centrifugal turbo blower with speed controller V+EXT
powerCOM2 → 5 A inline blade-fuse holder with 5 A fuse OUTEXT
powerNO2 → JGA25-370 12 V 12 RPM geared motor M+EXT

16. Connections between "fuse" and "ESP32"

FunctionfuseESP32
powerOUT → 3.3 V-input two-channel optocoupler relay module COM1EXT

17. Connections between "temperature_decoupling" and "ESP32"

Functiontemperature_decouplingESP32
digital1 → DS18B20 VCCEXT
digital2 → DS18B20 GNDEXT

18. Connections between "fan_pulldown" and "ESP32"

Functionfan_pulldownESP32
digitalP1 → 3.3 V-input two-channel optocoupler relay module IN1EXT
groundP2GND

19. Connections between "mixer_pulldown" and "ESP32"

Functionmixer_pulldownESP32
digitalP1 → 3.3 V-input two-channel optocoupler relay module IN2EXT
groundP2GND

Deploy the firmware

#define TINY_GSM_MODEM_SIM800
#include <Arduino.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <TinyGsmClient.h>
#include <Preferences.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>

constexpr int TEMP_PIN=4, MOISTURE_PIN=33, GSM_RX_PIN=16, GSM_TX_PIN=17;
constexpr int FAN_RELAY_PIN=25, MOTOR_RELAY_PIN=26;
constexpr bool RELAY_ACTIVE_LOW=false;
constexpr int RELAY_ON=RELAY_ACTIVE_LOW?LOW:HIGH, RELAY_OFF=RELAY_ACTIVE_LOW?HIGH:LOW;
// Placeholder endpoints: calibrate against this substrate before enabling alerts.
constexpr int MOISTURE_RAW_DRY=3000, MOISTURE_RAW_WET=1400;
constexpr bool MOISTURE_CALIBRATED=false;
constexpr uint32_t FAN_INTERVAL_MS=10800000UL, FAN_DURATION_MS=600000UL;
constexpr uint32_t MIX_INTERVAL_MS=21600000UL, MIX_DURATION_MS=120000UL;
constexpr uint32_t MATURITY_DURATION_MS=432000000UL;
Preferences batchPrefs;
uint32_t batchId=0;
void logRecord(const char *event);
OneWire wire(TEMP_PIN);
DallasTemperature temperatureSensors(&wire);
HardwareSerial SerialAT(2);
TinyGsm modem(SerialAT);
QueueHandle_t txQueue, commandQueue;
struct Sms { char text[161]; };
struct Command { bool newBatch; };
portMUX_TYPE statusMux=portMUX_INITIALIZER_UNLOCKED;
char sharedStatus[161]="Controller starting";
bool fanRunning=false,motorRunning=false,fanPulse=false,thermal=false;
bool tempValid=false,moistValid=false,maturityTiming=false,maturityReported=false;
bool conversionPending=false;
float coreTempC=NAN,rawTempC=NAN,moisturePercent=NAN;
int moistureRaw=0;
uint32_t lastFan=0,lastMix=0,fanStart=0,motorStart=0,lastSensor=0,conversionStart=0;
uint32_t maturityStart=0,lastAlert=0,lastCsv=0;
bool alertSent=false;
void setFan(bool on) {
 if(on==fanRunning)return;
 digitalWrite(FAN_RELAY_PIN,on?RELAY_ON:RELAY_OFF);fanRunning=on;
 Serial.printf("EVENT,%lu,fan,%s\n",millis(),on?"ON":"OFF");
 logRecord(on?"FAN_ON":"FAN_OFF");
}
void setMotor(bool on) {
 if(on==motorRunning)return;
 digitalWrite(MOTOR_RELAY_PIN,on?RELAY_ON:RELAY_OFF);motorRunning=on;
 Serial.printf("EVENT,%lu,mixer,%s\n",millis(),on?"ON":"OFF");
 logRecord(on?"MIXER_ON":"MIXER_OFF");
}
void notify(const String &s) {
 Sms m={};s.toCharArray(m.text,sizeof(m.text));
 if(xQueueSend(txQueue,&m,0)!=pdTRUE)Serial.println("SMS queue full; alert not queued");
 Serial.println(s);
}
// All modem operations belong to this task, never the actuator loop.
void gsmTask(void*) {
 Preferences prefs;prefs.begin("compost",false);
 String recipient=prefs.getString("phone","");
 SerialAT.begin(9600,SERIAL_8N1,GSM_RX_PIN,GSM_TX_PIN);
 vTaskDelay(pdMS_TO_TICKS(1000));
 bool ready=modem.restart() && modem.waitForNetwork(60000L);
 if(ready) {
  modem.sendAT("+CMGF=1");modem.waitResponse(3000L);
  modem.sendAT("+CNMI=2,2,0,0,0");modem.waitResponse(3000L);
 }
 uint32_t lastCheck=millis(),lastSmsAttempt=millis()-30000UL;
 String line,sender;bool body=false;
 for(;;) {
  if(millis()-lastCheck>=60000UL) {
   lastCheck=millis();
   if(!modem.testAT(1000L))modem.restart();
   ready=modem.waitForNetwork(10000L);
   if(ready) {
    modem.sendAT("+CMGF=1");modem.waitResponse(1000L);
    modem.sendAT("+CNMI=2,2,0,0,0");modem.waitResponse(1000L);
   }
   Serial.println(ready?"GSM registered":"GSM unavailable; retry in 60 s (automatic registration)");
  }
  while(SerialAT.available()) {
   char c=SerialAT.read();
   if(c=='\n') {
    line.trim();
    if(line.startsWith("+CMT:")) {
     int a=line.indexOf('"'),b=line.indexOf('"',a+1);
     sender=(a>=0&&b>a)?line.substring(a+1,b):"";body=true;
    } else if(body&&line.length()) {
     body=false;line.toUpperCase();
     // First phone pairs; existing owner alone may pair again or reset batch.
     if(line=="PAIR"&&sender.length()&&(!recipient.length()||recipient==sender)) {
      recipient=sender;prefs.putString("phone",recipient);
      modem.sendSMS(recipient,"Paired. Commands: STATUS, NEWBATCH.");
     } else if(sender==recipient&&recipient.length()) {
      if(line=="STATUS") {
       char snapshot[161];portENTER_CRITICAL(&statusMux);
       memcpy(snapshot,sharedStatus,sizeof(snapshot));portEXIT_CRITICAL(&statusMux);
       modem.sendSMS(recipient,String(snapshot));
      } else if(line=="NEWBATCH") {
       Command cmd={true};
       bool accepted=xQueueSend(commandQueue,&cmd,0)==pdTRUE;
       modem.sendSMS(recipient,accepted?"Batch reset queued.":"Reset queue full; try again.");
      }
     }
    }
    line="";
   } else if(c!='\r') {
    line+=c;if(line.length()>200){line="";body=false;}
   }
  }
  Sms m;
  if(ready&&recipient.length()&&millis()-lastSmsAttempt>=30000UL&&xQueuePeek(txQueue,&m,0)==pdTRUE) {
   lastSmsAttempt=millis();
   bool sent=modem.sendSMS(recipient,String(m.text));
   Serial.println(sent?"SMS accepted by modem (not delivery confirmation)":"SMS failed");
   if(sent)xQueueReceive(txQueue,&m,0);
  }
  vTaskDelay(pdMS_TO_TICKS(20));
 }
}
void readSensors(uint32_t now) {
 if(!conversionPending && now-lastSensor>=5000UL) {
  lastSensor=now;conversionStart=now;conversionPending=true;
  temperatureSensors.requestTemperatures();
 }
 if(!conversionPending || now-conversionStart<800UL)return;
 conversionPending=false;
 float t=temperatureSensors.getTempCByIndex(0);
 // Read only after a requested conversion has had 800 ms to finish.
 // 85 C is legitimate after conversion, not a permanent rejection value.
 rawTempC=t;
 tempValid=isfinite(t)&&t!=DEVICE_DISCONNECTED_C&&t>=-40&&t<=125;
 coreTempC=tempValid?t:NAN;
 moistureRaw=analogRead(MOISTURE_PIN);
 moistValid=moistureRaw>10&&moistureRaw<4085&&MOISTURE_RAW_DRY>MOISTURE_RAW_WET;
 moisturePercent=moistValid?constrain(100.0f*(MOISTURE_RAW_DRY-moistureRaw)/float(MOISTURE_RAW_DRY-MOISTURE_RAW_WET),0.0f,100.0f):NAN;
 Serial.printf("SAMPLE,%lu,%.2f,%d,%.1f,%d,%d\n",now,coreTempC,moistureRaw,moisturePercent,tempValid,moistValid);
}
// Uptime records only; capture externally for permanent storage.
void logRecord(const char *event) {
 const char *stamp="NO_RTC";
 char row[320]={};
 snprintf(row,sizeof(row),"%s,%lu,%lu,%s,%.2f,%d,%.1f,%d,%d,%d,%d,%d,%d,%d,%.2f",stamp,millis(),batchId,event,coreTempC,moistureRaw,moisturePercent,MOISTURE_CALIBRATED,tempValid,moistValid,fanRunning,motorRunning,thermal,maturityReported,rawTempC);
 Serial.print("RECORD,");Serial.println(row);
}
void setup() {
 Serial.begin(115200);
 // Preload OFF before enabling outputs; external pulldowns protect reset. 
 digitalWrite(FAN_RELAY_PIN,RELAY_OFF);digitalWrite(MOTOR_RELAY_PIN,RELAY_OFF);
 pinMode(FAN_RELAY_PIN,OUTPUT);pinMode(MOTOR_RELAY_PIN,OUTPUT);
 analogReadResolution(12);analogSetPinAttenuation(MOISTURE_PIN,ADC_11db);
 temperatureSensors.begin();temperatureSensors.setWaitForConversion(false);
 Serial.println("Bench mode: uptime only; schedules reset at boot; no onboard history.");
 batchPrefs.begin("batch",false);batchId=batchPrefs.getUInt("id",1);
 logRecord("BOOT_OBSERVATION_RESET");
 txQueue=xQueueCreate(8,sizeof(Sms));commandQueue=xQueueCreate(2,sizeof(Command));
 if(!txQueue||!commandQueue){Serial.println("Queue allocation failure; outputs OFF");while(true)delay(1000);}
 if(xTaskCreate(gsmTask,"gsm",8192,nullptr,1,nullptr)!=pdPASS){Serial.println("GSM task failed; outputs OFF");while(true)delay(1000);}
 lastFan=lastMix=millis();lastSensor=millis()-5000UL;
 Serial.println("RECORD columns: clock_status,uptime_ms,batch,event,temp_C,moisture_raw,moisture_index,calibrated,temp_valid,moist_valid,fan,mixer,thermal,indicator,raw_temp_C");
 Serial.println("CSV: SAMPLE,uptime_ms,temp_C,moisture_raw,relative_percent,temp_valid,moist_valid");
}
void loop() {
 uint32_t now=millis();Command cmd;
 if(xQueueReceive(commandQueue,&cmd,0)==pdTRUE){
  setMotor(false);
  fanPulse=false;
  // Do not clear a thermal safety latch just because a new batch is requested.
  setFan(thermal);
  maturityTiming=false;maturityReported=false;maturityStart=0;
  lastFan=lastMix=now;alertSent=false;lastAlert=now;
  batchId++;batchPrefs.putUInt("id",batchId);logRecord("NEWBATCH");
 }
 readSensors(now);
 // Invalid temperature stops mixing and latches any existing thermal override.
 if(tempValid){if(coreTempC>=60)thermal=true;else if(coreTempC<=58)thermal=false;}
 if(motorRunning&&(thermal||!tempValid||now-motorStart>=MIX_DURATION_MS))setMotor(false);
 if(fanPulse&&now-fanStart>=FAN_DURATION_MS)fanPulse=false;
 if(!thermal&&!fanPulse&&!motorRunning&&now-lastFan>=FAN_INTERVAL_MS){fanPulse=true;fanStart=lastFan=now;}
 setFan(thermal||fanPulse);
 if(!motorRunning&&!fanRunning&&tempValid&&now-lastMix>=MIX_INTERVAL_MS){motorStart=lastMix=now;setMotor(true);}
 if(!tempValid||coreTempC>32){maturityTiming=false;} else {
  if(!maturityTiming){maturityTiming=true;maturityStart=now;}
  if(!maturityReported&&now-maturityStart>=MATURITY_DURATION_MS){maturityReported=true;notify("Temperature indicator: <=32 C for 5 monitored days. NOT proof of maturity; validate in laboratory.");}
 }
 bool bad=!tempValid||!moistValid||(tempValid&&coreTempC>65)||
   (MOISTURE_CALIBRATED&&moistValid&&(moisturePercent<45||moisturePercent>70));
 if(!bad)alertSent=false;
 if(bad&&(!alertSent||now-lastAlert>=43200000UL)) {
  notify("Reactor warning: check sensors, temperature >65 C, or calibrated moisture outside 45-70%. Send STATUS.");
  alertSent=true;lastAlert=now;
 }
 char stamp[24];snprintf(stamp,sizeof(stamp),"up=%lus",(unsigned long)(now/1000UL));
 char status[161]={};
 snprintf(status,sizeof(status),"B%lu %s T=%.1fC M=%.0f%%(%s) raw=%d fan=%d mix=%d thermal=%d 5day=%d",batchId,stamp,coreTempC,moisturePercent,MOISTURE_CALIBRATED?"relative":"UNCAL",moistureRaw,fanRunning,motorRunning,thermal,maturityReported);
 portENTER_CRITICAL(&statusMux);memcpy(sharedStatus,status,sizeof(status));portEXIT_CRITICAL(&statusMux);
 if(now-lastCsv>=300000UL){lastCsv=now;Serial.printf("LOG,%lu,%s\n",now,status);logRecord("SAMPLE_5MIN");}
 delay(10);
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik