Skip to content

Smart incubator

The smart incubator is a Peltier-based incubator controlled over Wi-Fi by the node. Each unit is a self-contained device that regulates its temperature, runs a light schedule set on the node (a 24-hour day or a T-cycle of another length), reports humidity and light, and also behaves as an ethoscope sensor, so the node logs its readings like those of an ESP sensor. It replaces an earlier design based on nRF24L01 radios and an Arduino Uno.

Compared with the earlier design:

  • there is no message broker and no radio: units talk REST over Wi-Fi;
  • each unit has one ESP8266 board (WeMos D1 R2);
  • units announce themselves over mDNS as _incubator._tcp and _sensor._tcp;
  • the schedule lives on the node, which pushes it to the unit whenever it changes and pushes it again if the unit’s copy drifts (after a reboot, for example);
  • lights can fade in and out like dusk and dawn (“crepuscular” fading), switched on per incubator.

In bench tests a unit both heated and cooled, settled cleanly within its control band, and held the chamber about 6 °C below room temperature.

Block Part Notes
Microcontroller WeMos / LOLIN D1 R2 (ESP8266) 3.3 V logic
Peltier driver Cytron SHIELD-MD10 R2 Stacked on the D1 R2. PWM (JP5) on Arduino D6, which is WeMos D4 / GPIO2; direction (JP8) on Arduino D2, which is WeMos D0 / GPIO16. The Cytron board is labelled with Arduino pin names, and the D1 R2 is shifted by two, so Arduino Dn is WeMos D(n−2)
Temperature and humidity SHT31 I2C address 0x44
Light BH1750 I2C 0x23, calibrated lux
Clock backup DS3231 (or the older DS1307) I2C 0x68, battery-backed
LED panel Logic-level N-channel MOSFET on D8 (for example 30N06L) PWM, own supply; GPIO15 must be low at boot, which the gate pull-down ensures
Hot-side fan MOSFET on D7 On/off

The firmware README is the reference for the hardware, schematics and pin choices.

Every load has its own supply: the WeMos never supplies Peltier, LED or fan current. One common ground joins the WeMos, the MD10, the LED supply and the fan supply. Power up the WeMos first, then the Peltier and LED supplies. The firmware holds the PWM at zero at boot; a pull-down on D6 is a good extra safeguard.

The three sensors share one 3.3 V I2C bus. On the D1 R2 the bus is GPIO4 and GPIO5, available on several header positions: D2/D1 and A4/A5 are the same pins electrically. With the MD10 stacked, A4/A5 are the easier solder points, since the shield’s analogue header only passes them through.

Signal WeMos pin GPIO Goes to
SDA D2 (or A4) GPIO4 SDA of SHT31, BH1750 and DS3231
SCL D1 (or A5) GPIO5 SCL of SHT31, BH1750 and DS3231
V+ 3V3 VIN/VCC of the three modules
GND GND the grounds of the three modules

Pull-ups: one 4.7 kΩ resistor to 3.3 V on SDA and one on SCL. Most breakout boards carry their own: check with a multimeter, and if several boards have pull-ups, remove all but one set.

Addresses (default, alternative):

  • SHT31: 0x44 (jumper to 0x45 if needed, and change Sensors.cpp to match);
  • BH1750: 0x23 (0x5C with ADDR pulled high);
  • DS3231: 0x68 (TinyRTC and DS1307 modules use the same address).
WeMos 3V3 ---+----+----+
| | |
SHT31 | BH1750 | DS3231
GPIO4 (D2/A4) SDA-+----+----+---- 4.7 kΩ up to 3V3
GPIO5 (D1/A5) SCL-+----+----+---- 4.7 kΩ up to 3V3
WeMos GND ---+----+----+

The panel is switched on its low side, with PWM on the MOSFET gate. The MOSFET must be logic level: the gate is driven at 3.3 V, so a part such as the IRF540, designed for 5 V gate drive, does not turn fully on. Use a 30N06L, IRLZ44N, AOI514 or equivalent.

Net From To
LED PWM D8 (GPIO15) MOSFET gate, through a 220 Ω resistor
Gate pull-down MOSFET gate GND, 10 kΩ (keeps the MOSFET off while the ESP boots or resets)
LED + LED supply V+ (typically 12 V) anode side of the LED panel
LED − cathode side of the LED panel MOSFET drain
Source MOSFET source common GND
LED supply return LED supply GND common GND
12 V LED supply (+) ----[ LED panel + / − ]----+
|
MOSFET D ---------+
D8 (GPIO15) --[220 Ω]-- G
[10 kΩ from G to GND]
S --------- GND (common)
12 V LED supply (−) ---------------------------- GND (common)

GPIO15 must be low at boot or the ESP8266 does not start. The 10 kΩ gate pull-down keeps it low, which is why D8 suits a low-side MOSFET; do not add a pull-up to this line. The PWM runs at the ESP8266’s default 1 kHz. The firmware fades the light between 0 and max_light without blocking, with a gamma correction (γ = 2.2) so that the change in brightness looks even.

The fan is switched on and off, not through the MD10, whose H-bridge is reserved for the Peltier. The MOSFET circuit is the same as for the LEDs, without PWM. Leave the speed-sensor wire of a 3-wire 12 V fan unconnected; a 4-wire PWM fan can also be switched this way, ignoring its PWM input.

Net From To
Fan enable D7 (GPIO13) MOSFET gate, through a 220 Ω resistor
Gate pull-down MOSFET gate GND, 10 kΩ
Fan + 12 V fan supply fan red lead
Fan − fan black lead MOSFET drain
Source MOSFET source common GND
Flyback diode across fan + and fan − (cathode to +) 1N4148 or 1N4007, protects the MOSFET from the motor’s inductive kick
Fan supply return fan supply GND common GND
12 V fan supply (+) ---+--- fan red
|
▾ cathode
1N4148
▴ anode
|
fan black
|
MOSFET D
D7 (GPIO13) --[220 Ω]-- G
[10 kΩ from G to GND]
S --- GND (common)
12 V fan supply (−) ------- GND (common)
WeMos GND ──┬── MD10 logic GND
├── MD10 motor-supply GND (Peltier supply −)
├── LED supply GND
├── fan supply GND
└── I2C sensor GNDs (already on the WeMos GND header)

A missing common ground is the most common cause of a unit not working at first power-up: the H-bridge misfires, the MOSFETs float and the I2C bus looks dead. Bring all grounds to the WeMos GND header if you can.

The firmware is a cooperative loop that never blocks, because the Wi-Fi stack must be serviced all the time. The files are in accessories/hardware/smart_incubators/firmware/:

  • client_firmware_esp8266.ino: setup(), loop() and the scheduler;
  • Peltier.{h,cpp}: PID control with a ±0.3 °C dead band, a pause before reversing direction, a slew limit, and a safe state on sensor faults;
  • LightControl.{h,cpp}: non-blocking fader and light-dark scheduler with period, anchor and crepuscular fading;
  • Sensors.{h,cpp}: SHT31 and BH1750, read at most every 5 s to limit self-heating;
  • TimeKeeper.{h,cpp}: time from SNTP, with the DS3231 as backup;
  • Network.{h,cpp}: WiFiManager setup portal, mDNS and over-the-air updates;
  • Config.{h,cpp}: settings stored in /config.json on LittleFS;
  • Api.{h,cpp}: the REST interface.

It builds for esp8266:esp8266:d1_mini with ESP8266 core 3.1.2. Flash the first time over USB; later updates go over the air with build.sh <ip-or-host>.

The unit keeps regulating temperature and light without a network; time synchronisation and remote commands resume when Wi-Fi returns.

Each unit serves two interfaces on port 80.

Incubator (mDNS _incubator._tcp, polled by the node):

Method and path Purpose
GET /telemetry Live readings and state, including rtc (whether the DS3231 was found at boot)
GET /config, POST /config Read or update the stored settings
POST /command sync_time, set_time (epoch), set_light, identify, reboot
GET /health Wi-Fi, memory, time validity, sensor fault, uptime, rtc
GET /i2c_scan Scans addresses 0x03–0x77 and names the known devices that answer; the first thing to check when sensor_fault is true
GET /status A self-refreshing status page

Sensor (mDNS _sensor._tcp), the same as the ESP sensor:

Method and path Purpose
GET / Sensor JSON: id (MAC address), ip, name, location, temperature, humidity, light, alerts
GET /id {"id": "<MAC>"}
POST /set Change name or location

The node writes the name of the incubator a unit is bound to into the unit’s sensor location, so its readings are grouped under that incubator.

Units appear on the node’s Incubators page, with a live status column. Units found on the network but not yet bound to an incubator are listed in a banner, with a control to link each to an incubator record. The setup wizard (step 5) and the incubator editor set the schedule: lights on and off, period, anchor, fade-in and fade-out times, maximum light and crepuscular fading. The node pushes the schedule to the unit when a record is added, changed or re-anchored, and checks every minute that the unit’s schedule still matches. The light schedule is also stored with each experiment’s metadata.

The incubator code is a self-contained package, src/node/ethoscope_node/incubators/, which does not depend on the rest of the node software.

Labs that have incubators but no ethoscopes can run the same package on its own, in a minimal Python environment:

Terminal window
python -m venv inc-venv
inc-venv/bin/pip install --no-deps -e src/node
inc-venv/bin/pip install bottle zeroconf requests
inc-venv/bin/ethoscope-incubator-server --port 8090

Options: --db-path, --reconcile-interval, --no-reconcile and -d/--debug. The database is stored in ~/.ethoscope_incubators/incubators.db by default.

ethoscope-incubator talks to one unit over HTTP and needs only requests. A bare name such as incubator-01 gets .local appended; full host names and addresses are used as given.

Terminal window
ethoscope-incubator status incubator-01 # live telemetry
ethoscope-incubator status incubator-01 --watch 5 # refresh every 5 s
ethoscope-incubator status incubator-01 --json # machine-readable
ethoscope-incubator config incubator-01 # stored settings
ethoscope-incubator set incubator-01 set_temp=22 max_light=80
ethoscope-incubator monitor incubator-01 --interval 60 --duration 30m
ethoscope-incubator monitor incubator-01 --csv --output run.csv
ethoscope-incubator light incubator-01 50 # temporary brightness override
ethoscope-incubator reboot incubator-01
ethoscope-incubator time sync incubator-01 # force a time sync to the clock chip
ethoscope-incubator time set incubator-01 now # set the clock by hand
ethoscope-incubator i2c incubator-01 # I2C scan with device names
ethoscope-incubator health incubator-01 # quick liveness check
Command Purpose
status Live telemetry, once or refreshed with --watch
config Show the stored settings
set Change settings given as KEY=VALUE; numbers, booleans and null are converted, other values are passed as text
monitor Sample for a while, with --interval, --duration (such as 30s, 5m or 2h), and optionally --output and --csv
light Override the brightness until the next schedule step
reboot Restart the unit
time sync, time set Force a time sync to the clock chip, or set the clock (epoch seconds or now)
i2c Scan the I2C bus and name the devices found; the first check when telemetry shows sensor_fault or rtc: false
health Quick liveness check

The command exits with code 0 on success and 2 on an HTTP error. Every reading command accepts --json, for use with jq or in scripts. monitor stops cleanly on Ctrl+C and prints a table or CSV; CSV pairs well with --output file.csv.