GPIO and Wiring Map
Canonical pin assignment for the Formicarium controller (ESP32-WROOM-32 DevKit,
.NET nanoFramework). This file, firmware/Formicarium.Controller/Core/Config/PinMap.cs,
and the pin table on the dashboard's Build page (/build) must always agree. A mismatch
between the three is the most likely latent bug in this repo.
Constraints that drove these choices
These are not arbitrary preferences. Every one of them will silently break the build if ignored:
| Constraint | Consequence |
|---|---|
| GPIO 6–11 are wired to the SPI flash | Unusable. Touching them bricks the boot. |
| GPIO 34–39 are input-only and have no internal pull-ups | Fine for analog and for pushed signals; wrong for an open-collector sensor unless you add an external pull-up. |
| ADC2 is unavailable whenever WiFi is active | The capacitive soil probe must live on ADC1 (GPIO 32–36, 39). This is the single most common ESP32 sensor bug. GPIO 25/26/27 are ADC2 but are perfectly fine as digital outputs. |
| Strapping pins 0, 2, 5, 12, 15 are sampled at boot | Avoided for anything load-bearing. GPIO 2 is used only for the onboard status LED, which is harmless. |
| GPIO 1/3 are UART0 | Reserved for the console and flashing. |
| nanoFramework's ESP32 1-Wire is UART-backed, not bit-banged | It needs a COM port's RX and TX pins, tied together to the DS18B20 DQ line. See the wiring note below — this is the easiest detail in the whole build to get wrong. |
Assignments
| Function | GPIO | Kind | Notes |
|---|---|---|---|
| SHT31 ×2 — SDA | 21 | I2C1 | Configuration.SetPinFunction(21, DeviceFunction.I2C1_DATA) |
| SHT31 ×2 — SCL | 22 | I2C1 | ...I2C1_CLOCK. 100 kHz. Addresses 0x44 (outworld) / 0x45 (nest). |
| DS18B20 bus — RX | 16 | COM3_RX | Tied together with TX to the DQ line |
| DS18B20 bus — TX | 17 | COM3_TX | 3 probes share one bus, addressed by ROM ID |
| Soil moisture | 34 | ADC1_CH6 | Input-only pin — correct for analog. Must be ADC1. |
| IR beam RX — riser 1 | 27 | GPIO in | Internal pull-up, interrupt on falling edge |
| IR beam RX — riser 2 | 14 | GPIO in | Internal pull-up, interrupt on falling edge |
| IR beam RX — riser 3 | 13 | GPIO in | Internal pull-up, interrupt on falling edge |
| IR emitter enable | 19 | GPIO out | Powers all three emitters together |
| Heater | 25 | GPIO out | IRLZ44N low-side. 150 Ω gate, 10 kΩ pulldown. 12 V DC cable. |
| Refill pump (water → reservoir) | 26 | GPIO out | IRLZ44N + SS34 flyback. Fills the reservoir; never reaches the nest. |
| Feed pump (syrup → outworld) | 4 | GPIO out | IRLZ44N + SS34 flyback diode |
| Fan (outworld) | 33 | GPIO out | IRLZ44N + SS34 flyback diode |
| Nest exhaust fan | 18 | GPIO out | IRLZ44N + SS34 flyback. Gated on nest humidity, not outworld. |
| WS2812B ring data | 23 | RMT | All three riser-mouth rings on one data line, chained |
| Reservoir float switch | 32 | GPIO in | Internal pull-up, switch to ground. LOW = full. See polarity note below. |
| Status LED | 2 | GPIO out | Onboard DevKit LED — heartbeat and fault blink |
Free and unused: 0, 1, 3, 5, 12, 15, 35, 36, 39.
The float-switch polarity note
The reservoir switch is wired to ground and read against the internal pull-up, so a risen float reads LOW and means full. That direction is chosen deliberately, not for convenience:
- A broken wire or unplugged connector floats HIGH, which reads "not full", which asks for a
refill. That refill is bounded twice over — by
RefillMaxRunSecondsand by the fact that the reservoir physically cannot hold a harmful dose — so the failure is a non-event. - The opposite polarity would read "full" forever on a broken wire, silently stop hydrating the colony, and look completely healthy on the dashboard. That is the failure nobody notices until it is too late.
Fail-safe here means "fails toward a bounded action", not "fails toward doing nothing", because doing nothing is what kills a colony slowly.
The 1-Wire wiring note
nanoFramework drives 1-Wire through a UART, so the bus is not a single GPIO:
GPIO16 (COM3_RX) ---+
+---- DQ ---> all three DS18B20 probes (parallel)
GPIO17 (COM3_TX) ---+
|
4.7 kΩ
|
3V3
TX and RX are tied to the same node. The 4.7 kΩ pull-up to 3V3 is mandatory — without it
the bus reads all-ones and every probe enumerates as absent. All three probes share this
one bus and are told apart by their 64-bit ROM IDs, which are read once during bring-up and
recorded in Setpoints.
Cable routing — the payoff of putting Electronics in the middle of the stack
The electronics bay sits between the Colony (below) and the Outworld (above), so every run exits through a flat plate and reaches its destination in inches rather than feet. Since the column is a square section, every surface a cable or riser passes through is flat, which is what lets every penetration use a proper bulkhead seat or a clamped solvent weld:
Through the bay's bottom plate, into the Colony:
- DS18B20 — nest bottom
- DS18B20 — nest top
- SHT31-B (nest airspace, 0x45)
- Capacitive soil probe (into the Ytong core)
- Water supply line to the reservoir
Through the bay's top plate, into the Outworld:
- DS18B20 — outworld
- SHT31-A (outworld air, 0x44)
- Fan
- Feed line (sugar water)
- WS2812B rings ×3 — these do not penetrate at all; they sit inside the bay and shine up through frosted ring-windows in the outworld floor
- IR beam pairs ×3 — mounted at the riser mouths, wiring dropping back into the bay
Nest ventilation (no ant-accessible mechanism at all):
- A mesh-screened exhaust port through the nest ceiling into the bay, with the nest fan bonded over it on the bay side and wired to the bulkhead strip.
- A passive mesh-screened intake low on the nest wall, behind the opaque sleeve.
- Cross-flow therefore runs bottom to top, along the same axis as the thermal gradient. The only thing inside the nest is stainless mesh, which has nothing to fail — every moving part sits in the bay and is reachable without opening the colony.
Not penetrating anything:
- The 12 V heat cable is wrapped around the outside of the Colony section under an insulating sleeve. No seal, no moisture on the heater, no ant contact.
- The three WS2812B rings sit inside the bay and shine up through frosted ring-windows in the outworld floor. Light crosses the boundary; wiring does not.
Every penetration goes through a flat surface with a bulkhead fitting, a gland, or a solvent-welded collar. An O-ring cannot seat on a curve, and a riser welded to a curved wall cannot be clamped while it cures — which is the main reason the column is a square section rather than a tube.
Power
12 V 3 A PSU ──┬── heat cable (via MOSFET)
├── refill pump (via MOSFET)
├── feed pump (via MOSFET)
└── buck converter 12 V → 5 V ──┬── ESP32 VIN
├── outworld fan (via MOSFET)
├── nest exhaust fan (via MOSFET)
├── WS2812B rings
└── IR emitters (via enable)
All grounds common. The ESP32's 3V3 rail feeds only the logic-level sensors (SHT31, DS18B20 pull-up, soil probe, beam receivers, float switch) — never a motor or the heater.
Cross-checking this file
Core/Config/PinMap.cs and the pin table on the dashboard's Build page must agree with the
table above. Before wiring anything, read all three and confirm they match: a disagreement here
produces a system that boots, reports plausible numbers, and heats to the wrong probe.