Skip to content

ADR-009: Optional Door Hardware and Door Modes

Status: Accepted Date: 2026-03-12

Context

The in-berlin hackerspace door has additional hardware beyond the NUKI Smart Lock:

  1. Bolt sensor — momentary switch detecting the main lock bolt position (key-operated)
  2. Reed relay — detects whether the door is physically closed
  3. Bistable solenoid bolt — secondary lock, 12V DC, polarity-switched via H-bridge, 0.5s pulse to lock/unlock
  4. Door crack sensor — short pulse when door opens a few degrees (repurposed former doorbell trigger)
  5. Doorbell system — momentary button outside, bell (GPIO-driven) inside, duo-color LED outside

This hardware is specific to our installation. Other hackerspaces using OpenLatch may not have any of these components. The firmware must maintain a consistent door state model regardless of whether the hardware is present.

Decision

Two-layer architecture

Layer 1: Door Hardware (door_hardware.h) — optional, compiled only with -DUSE_DOOR_HARDWARE. Thin GPIO abstraction: init pins, read sensors, drive solenoid/bell/LED. No logic.

Layer 2: Door Controller (door_controller.h) — always compiled. Manages door modes and state machine. Consumes sensor events (from hardware layer or software commands). Drives outputs through callbacks. Works without door hardware by accepting software-triggered state transitions.

This separation means: - With hardware: sensors feed real data into the controller, outputs drive real GPIOs - Without hardware: the controller still tracks modes and state, driven by NFC access events and API commands. Door is assumed closed after unlock timeout. No solenoid, bell, or LED.

Door Modes

When the space is open (not in lockup), occupants select one of four door modes via 1-2 momentary buttons inside the space (active only with USE_DOOR_HARDWARE) or via the admin API:

Mode Solenoid Doorbell press Door crack External LED
OPEN_SILENT Unlocked Ignored Ignored Green
OPEN_NOTICE Unlocked Ring Ring Green
GUARDED_AUTO Locked Ring → delay → unlock → relock on close Ring Red; blink green during unlock window
GUARDED_MANUAL Locked Ring (member goes to door) Ring Red

When the space is closed (lockup mode): NUKI locked, solenoid locked, doorbell ignored, external LED red. NFC/TOTP access still works for authorized members.

In all modes: a member presenting a valid NFC key or TOTP code always gains access, regardless of door mode.

Solenoid control rules

  • Unlock: set H-bridge A=HIGH, B=LOW for 500ms, then both LOW
  • Lock: set H-bridge A=LOW, B=HIGH for 500ms, then both LOW
  • Auto-relock (GUARDED_AUTO): after unlock, wait for door open (reed) then door close (reed), then lock. If door never opens within 15 seconds, relock anyway.
  • Reed debounce: 100ms debounce before confirming door closed/open state changes

Bolt sensor as lockup hint

If the bolt sensor detects "locked" (someone turned the key from outside) and the space is currently open, the firmware logs an event and can optionally trigger lockup mode. This handles the case where someone locks up physically without pressing the lockup button. The exact behavior (auto-lockup vs. log-only) is configurable.

Inside control panel (I2C bus)

The inside user interface (mode buttons, status LEDs, future LCD) is connected via an SX1509 I2C GPIO expander rather than using ESP32 GPIOs directly. This:

  • Frees ESP32 GPIOs for door-mounted, time-critical hardware
  • Puts all inside-UI on a single I2C bus (2 wires: SDA/SCL)
  • Makes the control panel physically separate and easy to relocate
  • Leaves room for a future I2C LCD display that could replace the status LEDs with richer feedback (mode name, member count, last event, etc.) — same bus, no rewiring

The I2C bus uses GPIO 21 (SDA) / GPIO 22 (SCL), shared with the PN532 in dev mode. The SX1509 INT pin connects to an ESP32 GPIO for interrupt-driven button reads (no polling needed).

SX1509 pin allocation (16 pins, active-low inputs with internal pull-ups):

SX1509 Pin Function Notes
0 Mode button A Tactile momentary switch, cycles forward through door modes
1 Mode button B Tactile momentary switch, cycles backward (or independent toggle)
4 LED: OPEN_SILENT Green, indicates current mode
5 LED: OPEN_NOTICE Green
6 LED: GUARDED_AUTO Yellow/amber
7 LED: GUARDED_MANUAL Yellow/amber
8-15 Reserved Future LCD buttons, additional indicators

The SX1509 supports hardware PWM on all pins, so LEDs can pulse/breathe to indicate the active mode rather than just on/off.

Mode cycling: button A short press cycles SILENT → NOTICE → GUARDED_AUTO → GUARDED_MANUAL → SILENT. The corresponding LED lights up. Button B cycles backward. The exact UX (cycle vs. independent toggles) can be refined once the panel is in use.

Without USE_DOOR_HARDWARE

The door controller still manages all four modes and state transitions. Sensor inputs that are unavailable use safe defaults:

Sensor Default without hardware Effect
Bolt sensor Assumed unlocked No auto-lockup hint; lockup requires button or API
Reed relay Door assumed closed after auto-relock timeout GUARDED_AUTO still works with timeout-based relock
Door crack sensor No events No entry notifications
Doorbell button No events Guests must be let in via other means

This means: - Space open (default: OPEN_SILENT) — NFC/TOTP access unlocks NUKI - Space closed (lockup) — NFC/TOTP access unlocks NUKI for authorized members - GUARDED modes — still functional via NUKI-only locking; solenoid commands become no-ops - Door modes can be set via API; modes that rely on missing hardware degrade gracefully

The lockup button (GPIO 33, always present) still triggers lockup mode.

Other hackerspaces can also choose a middle ground: use jumpers or fixed wiring to simulate sensors (e.g., tie reed relay input to GND = "always closed") without needing the full hardware. The compile flag simply controls whether GPIO init and ISR registration happen.

GPIO Assignments

All behind USE_DOOR_HARDWARE. Chosen to avoid conflicts with existing pins (16/17 Wiegand, 21/22 PN532 I2C shared, 25/26/27 LED/buzzer, 33 lockup button).

ESP32 GPIOs — door-mounted hardware (time-critical)

Component Direction GPIO Notes
Bolt sensor Input 34 Input-only, needs external pull-up
Reed relay Input 35 Input-only, needs external pull-up
Door crack sensor Input 36 Input-only, edge-triggered, ext. pull-up
Doorbell button Input 39 Input-only, needs external pull-up
Solenoid A Output 4 H-bridge pin 1
Solenoid B Output 5 H-bridge pin 2
Bell Output 18 Simple on/off
External LED grn Output 19 Duo-color LED outside
External LED red Output 23 Duo-color LED outside
SX1509 INT Input 15 Interrupt from I2C expander

I2C bus — inside control panel

Bus GPIO Notes
I2C SDA 21 Shared with PN532 in dev mode
I2C SCL 22 Shared with PN532 in dev mode

SX1509 at default address 0x3E. Pin allocation see "Inside control panel" section above.

Note: GPIO 34-39 are input-only on ESP32 and lack internal pull-ups. External 10k pull-up resistors to 3.3V are required for dev breadboard setups.

PlatformIO Environment

New environment esp32dev-inberlin extends the production esp32dev with the door hardware flag:

[env:esp32dev-inberlin]
extends = env:esp32dev
build_flags =
    ${env:esp32dev.build_flags}
    -DUSE_DOOR_HARDWARE
lib_deps =
    ${env:esp32dev.lib_deps}
    sparkfun/SparkFun SX1509 IO Expander@^3.0.4

Consequences

  • The door controller state machine is testable on native (no hardware dependency)
  • Other hackerspaces get a working system with just NUKI — no door hardware needed
  • The in-berlin installation gets full sensor feedback, secondary locking, and guest doorbell
  • Inside control panel is on I2C (SX1509) — physically separate, easy to relocate
  • Mode cycling provides physical UX without requiring the admin UI
  • The two-layer split keeps controller logic hardware-agnostic
  • I2C bus is ready for a future LCD display (same bus, no rewiring)
  • PN532 (dev) and SX1509 (in-berlin) share I2C without conflict (different addresses)