إنتقل إلى المحتوى الرئيسي

CPS STM32 Door Node (PN532 RFID + solenoid)

This runbook brings up the STM32F401 door node: a WeAct Blackpill wired to a real PN532 RFID reader and a 12V solenoid lock driven through an XY-MOS N-channel MOSFET module. Unlike the bench node (cps-stm32-bench, which simulates badges with a button and only blinks an LED), the door node reads real cards and actuates a real lock, with the backend as the access authority. See ADR 051.

It keeps the same USB CDC-ACM transport and aurora-firmware-contracts wire format as the bench node, so the existing serial bridge and MQTTEdgeConsumer ingest it unchanged. The bench node project is left in place; the door node is a separate firmware project (stm32f401_door_node).

Hardware

  • WeAct STM32F401CCU6 Blackpill (256 KB flash), connected by USB-C.
  • ST-Link V2 on the SWD header (SWDIO, SWCLK, GND, 3V3) for flashing and logs.
  • PN532 RFID reader on I2C1, set to I2C mode with its onboard switches.
  • XY-MOS N-channel MOSFET module driving a 12V solenoid lock from a 12V supply.

Wiring

STM32F401Connects toNotes
PA11 / PA12USB-C D- / D+CDC-ACM serial to host
PA13 / PA14ST-Link SWDIO / SWCLKflash and RTT logs
PB6 (SCL), PB7 (SDA)PN532 SCL / SDAPN532 in I2C mode; also wire its 3V3 and GND
PB0XY-MOS TRIG / PWMlogic-level gate drive
GNDXY-MOS GNDcommon ground between the board and the module
PA0request-to-exit button to GNDoptional local egress
(12V)XY-MOS VIN+ / VIN-solenoid supply
(solenoid)XY-MOS OUT+ / OUT-add a flyback diode (e.g. SS54 or 1N5408) across the solenoid

The solenoid is fail-secure, energize-to-unlock: the gate is driven high to open for the unlock window, then released (locked). Loss of power leaves the door locked. The flyback diode is required to absorb the inductive kickback when the solenoid de-energizes.

What the firmware does

  • Presents a USB serial port and emits one line per event as <topic>\t<payload> using the shared aurora-firmware-contracts format.
  • Polls the PN532 for ISO14443A cards and publishes the real UID (uppercase hex) on aurora/access/<device_id>/badge_event. A short cooldown de-duplicates a card left on the reader.
  • Does not self-unlock on a badge read. The backend decides (see below).
  • Drives the solenoid and the PC13 LED from the lock state: locked (gate low, LED off), unlocked (gate high, LED on for the unlock window, then auto-relock), lockdown (gate low, LED blinking).
  • PA0 is a local request-to-exit (egress) button that unlocks locally.
  • Publishes periodic status (every 30 s) and real MCU die-temperature telemetry (every 10 s).
  • Accepts commands written back over serial: unlock, lock, lockdown, lockdown_clear, keepalive. A fail-secure watchdog relocks the door if the host link goes silent for 70 s.

Flash and observe

From firmware/embassy-stm32/projects/stm32f401_door_node:

export DEVICE_ID="stm32f401-door-01"
cargo run --release # probe-rs flashes via ST-Link and streams defmt RTT

or just fw flash-stm32f401-door (set DEVICE_ID first). The RTT log shows boot, the PN532 firmware version, status, telemetry, and a line per card read. A /dev/ttyACM* serial port also appears for the USB CDC interface, enumerated as "AuroraSOC Door Node". To watch the raw framed lines directly:

cat /dev/ttyACM0 # topic<TAB>payload lines on the status/telemetry interval

Run the host bridge

With the stack up (Mosquitto, API, console), run the bridge so the device lines reach the broker. The broker exposes a loopback plaintext listener on 127.0.0.1:1883 for bench use, so the bridge needs no client certificate:

export SERIAL_BRIDGE_ENABLED=true
export SERIAL_BRIDGE_PORT=/dev/ttyACM0
export SERIAL_BRIDGE_DEVICE_ID=stm32f401-door-01
export MQTT_HOST=127.0.0.1 MQTT_PORT=1883 MQTT_USE_TLS=false
python -m aurorasoc.tools.cps.serial_bridge

The backend MQTTEdgeConsumer is subscribed on the mTLS listener of the same broker, so device lines published on 1883 reach it and commands it publishes reach the bridge. When the API itself runs with SERIAL_BRIDGE_ENABLED=true and the device mapped in, the bridge starts inside the API lifespan instead.

Backend-authorized unlock

The door node never opens on its own. The full loop is:

  1. A card is tapped. The node publishes a badge_event with the real UID.
  2. MQTTEdgeConsumer authorizes the UID against the access policy and writes an access event (granted or denied) that appears in the console.
  3. On a grant, the backend dispatches an unlock command to aurora/access/<device_id>/command. The bridge forwards it to the board and the solenoid opens for the unlock window, then auto-relocks.
  4. On a denial, an access_denied CPS alert is raised; the lock stays shut.

Operator commands from the console (Access Control, Commands tab) use the same command topic, so Unlock, Lock, Lockdown, and Clear Lockdown reach the board too.

End-to-end check

  1. With the bridge running, the device appears in GET /api/v1/cps/devices and on the CPS page; temperature_c telemetry lands on the sensors topic.

  2. In the console, open Access Control, read the UID of a tapped card from the Live Event Feed, then create a Badge with that UID and assign it a zone.

  3. Tap the card again. The feed shows GRANTED, the backend sends unlock, and the solenoid opens (LED on) for the unlock window before relocking. An unknown card shows DENIED and raises an access_denied alert.

  4. Drive the lock manually to confirm the command path:

    mosquitto_pub -h 127.0.0.1 -p 1883 \
    -t aurora/access/stm32f401-door-01/command -m unlock

Notes

  • The PN532 is wired for reading only; no secure element is fitted, so attestation requests are not answered and the device stays PENDING by design.
  • Pulling the USB cable or stopping the bridge trips the fail-secure watchdog after 70 s, which relocks the door and raises a failsecure_lock alert.