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._tcpand_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.
Hardware
Section titled “Hardware”| 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.
Wiring
Section titled “Wiring”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.
I2C bus: SHT31, BH1750 and DS3231
Section titled “I2C bus: SHT31, BH1750 and DS3231”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 to0x45if needed, and changeSensors.cppto match); - BH1750:
0x23(0x5Cwith ADDR pulled high); - DS3231:
0x68(TinyRTC and DS1307 modules use the same address).
WeMos 3V3 ---+----+----+ | | | SHT31 | BH1750 | DS3231GPIO4 (D2/A4) SDA-+----+----+---- 4.7 kΩ up to 3V3GPIO5 (D1/A5) SCL-+----+----+---- 4.7 kΩ up to 3V3 WeMos GND ---+----+----+LED panel: D8 to a logic-level MOSFET
Section titled “LED panel: D8 to a logic-level MOSFET”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.
Hot-side fan: D7 to a MOSFET
Section titled “Hot-side fan: D7 to a MOSFET”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)Common ground
Section titled “Common ground”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.
Firmware
Section titled “Firmware”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.jsonon 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.
REST interface
Section titled “REST interface”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.
On the node
Section titled “On the node”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.
Without ethoscopes: the standalone server
Section titled “Without ethoscopes: the standalone server”Labs that have incubators but no ethoscopes can run the same package on its own, in a minimal Python environment:
python -m venv inc-venvinc-venv/bin/pip install --no-deps -e src/nodeinc-venv/bin/pip install bottle zeroconf requestsinc-venv/bin/ethoscope-incubator-server --port 8090Options: --db-path, --reconcile-interval, --no-reconcile and -d/--debug. The database is stored in ~/.ethoscope_incubators/incubators.db by default.
Command-line client
Section titled “Command-line client”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.
ethoscope-incubator status incubator-01 # live telemetryethoscope-incubator status incubator-01 --watch 5 # refresh every 5 sethoscope-incubator status incubator-01 --json # machine-readableethoscope-incubator config incubator-01 # stored settingsethoscope-incubator set incubator-01 set_temp=22 max_light=80ethoscope-incubator monitor incubator-01 --interval 60 --duration 30methoscope-incubator monitor incubator-01 --csv --output run.csvethoscope-incubator light incubator-01 50 # temporary brightness overrideethoscope-incubator reboot incubator-01ethoscope-incubator time sync incubator-01 # force a time sync to the clock chipethoscope-incubator time set incubator-01 now # set the clock by handethoscope-incubator i2c incubator-01 # I2C scan with device namesethoscope-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.