An ESP32-based controller for a rolloff observatory roof driven by a garage door opener. Implements the ASCOM Alpaca Dome interface so any Alpaca-compatible astronomy application (N.I.N.A., Cartes du Ciel, etc.) can open, close, and monitor the roof. A built-in web page provides manual control from any browser on the local network.
This driver is built for a roll-off roof pulled by a standard garage door opener, controlled the same way its wall button controls it: a single momentary contact that both opens and closes. There is no separate open input and close input. Each press cycles the opener through its sequence — move, stop, move back — and the relay imitates one press with a 250 ms pulse on GPIO 18.
Everything unusual about the firmware follows from that one fact:
- The device cannot command a direction. It can only say "press the button". It infers what the roof is doing from the last command it issued and the two limit switches.
AbortSlewpulses only while the roof is actually moving. On a toggle input, pressing the button on a stationary roof would start it, so an abort on a stopped roof deliberately does nothing.- Opening an already-open roof does nothing. The commands check the limit switches first, because a stray pulse would close it.
- The two limit switches are the only ground truth. Between them the roof's position is unknown, which is why a 2-minute movement watchdog exists.
If your opener exposes separate open and close inputs, or accepts a position command, this firmware needs modification rather than configuration.
- ASCOM Alpaca Dome driver (IDomeV2, interface version 2) on port 11111
- Alpaca UDP auto-discovery on port 32227 — no manual IP entry needed in N.I.N.A.
- Browser control/status page on port 80, plus a diagnostics page at
/diag - 1.28″ GC9A01 round TFT (240×240) showing shutter status, IP address and whether Alpaca is polling
- Two limit switches (OPEN and CLOSED positions)
- Relay pulse (250 ms) to toggle the motor controller
- 2-minute movement watchdog — sets error state if a limit switch is never reached
- WiFi credentials kept in a gitignored file — safe to push to GitHub
The roof has to work unattended overnight, so several failure modes are handled explicitly:
- Persistent HTTP connections on the Alpaca port. N.I.N.A. polls a dome
several times a second. Arduino's
WebServercloses the connection after every response, and lwIP holds each closed socket for about 140 seconds, so the device can only sustain roughly 7 new connections a minute. Port 11111 therefore runs on ESP-IDF'sesp_http_server, which keeps the connection open — an entire session costs one connection instead of thousands. The status page reports requests per connection so you can confirm it. - Hardware task watchdog. The chip resets if
loop()stops running. - WiFi supervisor. Notices a dropped association from driver events, re-associates, and reboots if it cannot recover. It never reboots while the roof is moving.
- Poll-stall supervisor. Catches the failure the WiFi supervisor can't
see: the association stays up, but traffic to this one device blackholes
and the client's polls time out. (The RG-11 safety monitor lost N.I.N.A.
this way twice in one night with nothing in its log.) Once a client has
proved itself live —
PUT Connected=trueor a status poll — 35 s of silence on the Alpaca API logspolls stopped (link up), 70 s forces a re-association, and 10 min disarms so a N.I.N.A. that was simply closed doesn't cause reconnect flapping overnight.PUT Connected=falsedisarms immediately. The status page shows the last poll age and stall count, and the display'sAlpaca:OKturns toAlpaca:--after 30 s of silence. - Diagnostics that survive a reboot. Boots, WiFi drops with their 802.11 reason codes, roof commands, and poll stalls (with the outage length when polls resume) are logged to RTC memory, which outlives a watchdog reset. A power cycle clears it deliberately, so the counters always mean "since mains was applied".
- The display is optional. The SPI link to the panel is write-only, so the firmware simply draws whether or not a display is plugged in. An IPS TFT does not burn in, so unlike the earlier OLED it needs no drift or idle-blank tricks.
| Part | Description |
|---|---|
| ESP32 DevKit-C | 38-pin, USB-C (ESP32-WROOM-32) |
| Display | 1.28″ GC9A01 round TFT, 240×240, SPI, 7-pin header (VCC GND SCL SDA DC CS RST) |
| Limit switch ×2 | Normally-open (NO) momentary switches |
| Relay module | Phidgets 3054 SSR (500 mA, 40 VDC / 28 VAC), triggered by a logic-HIGH pulse. Its output wires across the garage door opener's wall-button terminals, in parallel with the existing button |
| Garage door opener | Any opener driven by a single momentary contact that opens, stops, and closes in sequence |
LEFT SIDE RIGHT SIDE
───────────────────────────────── ─────────────────────────────
3V3 ── TFT VCC ┐ VIN
GND ── TFT GND │ GND Limit sw common (both)
D15 ── TFT SCL │ display header D13
D2 ── TFT SDA │ seats directly on D12
D4 ── TFT DC │ these seven pins D14
RX2 ── TFT CS │ D27
TX2 ── TFT RST ┘ D26
D5 D25
D18 (GPIO18) ── Relay signal D33 (GPIO33) ── Limit sw OPEN
D19 D32 (GPIO32) ── Limit sw CLOSED
D21 D35
RX0 D34
TX0 VN
D22 VP
D23 EN
GND ── Relay GND
The display seats directly onto the first seven pins of the left rail — no jumpers. Its header order matches the board silkscreen one-to-one, the same arrangement used on the RG-11 safety monitor.
The module labels its SPI pins with I²C names:
SCLis SCK andSDAis MOSI. It is SPI, not I²C. There is noBLKpin, so the backlight is permanently on.
Display pin → ESP32 header pin (GPIO)
──────────────────────────────────────────
VCC → 3V3
GND → GND
SCL → D15 (GPIO 15, SCK)
SDA → D2 (GPIO 2, MOSI)
DC → D4 (GPIO 4)
CS → RX2 (GPIO 16)
RST → TX2 (GPIO 17)
Pin numbers are set in platformio.ini as TFT_eSPI build
flags, so no User_Setup.h edit is needed and the config travels with the repo.
GPIO 2 also drives the DevKit's onboard LED, so it flickers with SPI traffic —
harmless. GPIO 2 and 15 are strapping pins, but the display is a high-impedance
input on those lines, so flashing and booting are unaffected.
| ESP32 pin | Signal | Notes |
|---|---|---|
| GPIO 32 (D32) | Limit switch — CLOSED | Pull-up enabled; switch other terminal → GND |
| GPIO 33 (D33) | Limit switch — OPEN | Pull-up enabled; switch other terminal → GND |
| GPIO 18 (D18) | Relay signal | Pulses HIGH for 250 ms, imitating one press of the opener's wall button |
| GPIO 15 (D15) | TFT SCL (SPI clock) | Display seats on the first seven left-rail pins |
| GPIO 2 (D2) | TFT SDA (SPI MOSI) | |
| GPIO 4 (D4) | TFT DC | |
| GPIO 16 (RX2) | TFT CS | |
| GPIO 17 (TX2) | TFT RST | |
| 3V3 | TFT VCC | |
| GND | TFT GND, relay GND, switch common |
The two limit-switch GPIOs (D32/D33) are adjacent on the right rail.
git clone https://github.com/exploded/ESP32-Rolloff.git
cd ESP32-Rolloffcp include/wifi_credentials.h.example include/wifi_credentials.hEdit include/wifi_credentials.h and fill in your SSID and password:
#define WIFI_SSID "YourSSID"
#define WIFI_PASS "YourPassword"wifi_credentials.h is listed in .gitignore and will never be committed.
Open the project in PlatformIO (VS Code extension or CLI) and upload:
pio run --target uploadRequired libraries (installed automatically by PlatformIO via platformio.ini):
bodmer/TFT_eSPI(configured by thebuild_flagsblock inplatformio.ini)
Open http://<ESP32-IP>/ in any browser on the same network. The page shows shutter status, switch states, link health, and Open / Close buttons. It polls a small JSON endpoint every 10 seconds rather than reloading itself, so leaving a tab open costs the device very little.
http://<ESP32-IP>/diag shows the reset reason, boot count, and the event log described under Reliability.
| Path | Port | Purpose |
|---|---|---|
/ |
80 | Status and control page |
/status.json |
80 | Status as JSON, polled by the page |
/cmd?a=open|close|abort |
80 | Same-origin roof control used by the buttons |
/diag |
80 | Diagnostics and event log |
/api/v1/dome/0/… |
11111 | ASCOM Alpaca Dome API |
/management/… |
11111 | Alpaca management API |
scripts/monitor-rolloff.ps1 logs availability and the device's own counters to
a CSV. It is read-only and cannot move the roof. Run it on the observatory PC
and leave it going:
.\scripts\monitor-rolloff.ps1 -Ip <ESP32-IP>It probes once a minute by design. Do not speed it up — a faster prober becomes a significant part of the load it is trying to measure.
The device advertises itself via Alpaca UDP discovery — most clients will find it automatically. If you need to enter it manually:
| Setting | Value |
|---|---|
| IP address | assigned by your router (shown on the display) |
| Alpaca port | 11111 |
| Device type | Dome |
| Device number | 0 |
Connect at 115200 baud to see startup messages including the assigned IP address.
The round panel uses the same scheme as the RG-11 safety monitor, so the two read alike side by side: a coloured ring at the rim around a black centre.
╭───────────────╮
╭─┤ coloured ├─╮ ring + headline colour:
╱ ╰───────────────╯ ╲ green OPEN
│ OPEN │ blue CLOSED
│ 192.168.1.38 │ amber OPENING / CLOSING
│ Alpaca:OK T:1234 │ red ERROR
╲ -62dBm up 318m ╱
╰───────────────────╯
| Line | Meaning |
|---|---|
| Headline | Shutter status: OPEN / CLOSED / OPENING / CLOSING / ERROR |
| IP address | Or WiFi lost 42s while the supervisor is reconnecting |
Alpaca:OK / Alpaca:-- |
A client polled port 11111 within the last 30 s — shows on the wall when N.I.N.A.'s polls stop reaching the device. T: is the Alpaca request count since boot |
| Footer | WiFi RSSI and uptime in minutes |
The whole frame is drawn into an off-screen sprite and pushed in one go, so it
refreshes twice a second without flicker. Boot progress (WiFi connecting...,
WiFi FAILED, Restarting) is shown as a two-line message.
| Port | Protocol | Purpose |
|---|---|---|
| 80 | TCP/HTTP | Browser status + control page |
| 11111 | TCP/HTTP | ASCOM Alpaca Dome API |
| 32227 | UDP | Alpaca auto-discovery |
| GPIO | Direction | Function |
|---|---|---|
| 2 | Output | TFT SDA (SPI MOSI) — also the onboard LED |
| 4 | Output | TFT DC |
| 15 | Output | TFT SCL (SPI clock) |
| 16 | Output | TFT CS |
| 17 | Output | TFT RST |
| 18 | Output | Relay pulse (HIGH = active) |
| 32 | Input | Limit switch CLOSED (active LOW) |
| 33 | Input | Limit switch OPEN (active LOW) |

