In this Interesting Arduino project, we are going to build a Prison cell monitoring system using a mmWave human presence detection sensor and Arduino and alerting system using an high power industrial AC Loud siren with strobe light.
The first seconds after a prisoner escapes their cell are the most critical. Before guards notice, before CCTV is reviewed, before a head count — the window of escape is already open. Conventional motion-based PIR sensors are categorically wrong for this task: they detect movement, not presence. A person who has already exited and is now still elsewhere will never trip a PIR sensor. More critically, a sleeping prisoner inside the cell constantly fails PIR detection, generating false positives for absence every few minutes.
This project takes a fundamentally different approach using the Ai-Thinker RD-03 a 24 GHz FMCW millimeter-wave radar module that senses human presence by detecting biological micro-motion: the 0.1–0.5 mm chest displacement caused by breathing. A sleeping prisoner is fully detected. A motionless prisoner is fully detected. An empty cell reads as genuinely absent. The alarm fires only when the cell is truly empty.
The interface is deliberately minimal. The RD-03’s OT2 pin is its sole output to the Arduino — one wire carrying a simple HIGH/LOW voltage. No UART protocol, no baud rate, no framing, no library, no parsing. Arduino reads this pin using analogRead() on an analog input and applies a voltage threshold comparison to reliably interpret the 3.3V signal without any external level shifter. This is the cleanest, most reliable possible interface for a safety-critical system.
When sustained absence is confirmed, the Arduino triggers an optocoupler relay which closes the AC circuit to a full industrial strobe siren (110–120 dB) audible output plus a high-visibility strobe light, the type used in industrial safety applications.
Table of Contents
Working Principle
The system operates as a five-state machine. Understanding each state and its transition conditions is essential before building the circuit.
Boot & Stabilization (5 seconds)
On power-up, the sketch enforces a 5-second delay. During this time the RD-03’s internal DSP initializes, the relay is kept de-energized (siren off), and both LEDs blink to signal boot-in-progress. No OT2 readings are acted on during this phase, preventing spurious triggers while the sensor’s radar field is establishing.
Arming Phase – Initial Presence Confirmation
After boot, the system waits for at least one confirmed HIGH reading on OT2 (prisoner locked inside, cell occupied) before arming. The green LED blinks slowly during this phase. This prevents the alarm from firing if the system restarts while the cell is legitimately empty during a transfer or shift change. Only when OT2 reads HIGH does the system transition to active monitoring and the green LED goes solid.
Armed Monitoring – OT2 Polled Every 100 ms
The core operating state. Every 100 ms (10 Hz), Arduino calls analogRead(A0) and compares the result against a threshold voltage. As long as OT2 is HIGH (≥ 3.3V from the sensor, mapping to analogRead ≥ 675/1023), the cell is confirmed occupied, green LED is on, relay is off, and siren is silent. This continues indefinitely. Note: the RD-03’s onboard “target disappearing delay” (configured to 5 seconds via the host computer tool) means OT2 stays HIGH for 5 seconds after the last valid detection, providing hardware-level debounce against brief radar dropout.
Debounce Phase – 10-Second Software Confirmation
When OT2 first reads LOW (analogRead < 675), the sketch does not immediately alarm. It starts a 10-second software debounce timer. If OT2 returns HIGH within those 10 seconds, the timer resets and monitoring continues – this absorbs any brief sensor dropout. Only if OT2 remains LOW for the full 10 seconds does the system escalate to alarm. Combined with the sensor’s 5-second hardware delay, total confirmed absence before alarm = 15 seconds minimum.
Alarm – Relay Fires, AC Siren Activates
After 15+ continuous seconds of confirmed absence, Arduino writes LOW to the relay control pin (active-LOW trigger). The optocoupler relay coil energizes, closing the Normally Open contact and completing the AC 110V circuit. The industrial strobe siren activates at full volume and strobe rate. A red LED illuminates. The alarm is latched it will not self-reset even if the prisoner returns. Only a physical press of the officer’s reset button clears it, forcing human acknowledgement of every alarm event.
Why analogRead() Instead of digitalRead()?
The RD-03 OT2 pin outputs 3.3V for HIGH and 0V for LOW. The Arduino Nano operates at 5V logic. The ATmega328P datasheet specifies a minimum HIGH input threshold (VIH) of 0.6 × VCC = 0.6 × 5V = 3.0V. While 3.3V is technically above this threshold and will consistently be read as HIGH by digitalRead(), the margin is only 0.3V — potentially narrow in noisy environments like a facility with electrical machinery and long cable runs.
Using analogRead(A0) instead is the engineering-correct solution. analogRead() measures the actual voltage and returns a 10-bit value (0–1023) where 1023 = 5V. A 3.3V signal maps to approximately 675 (3.3 ÷ 5.0 × 1023). By comparing the reading against a threshold of 600 (equivalent to ~2.93V), we have over 400 ADC counts of clearance above the threshold, making the detection immune to noise, minor cable resistance drops, and supply voltage fluctuations. This is a software-only level tolerance solution — no hardware level shifter needed, no extra component, no cost.
Required Components
All components below are available on Amazon.com or DigiKey
| Component | Model / Spec | Qty |
|---|---|---|
| Microcontroller | Arduino Nano (ATmega328P, 5V, 10-bit ADC) | 1 |
| mmWave Radar Sensor | Ai-Thinker RD-03 (24 GHz FMCW, DIP 2.54 mm, 5-pin) | 1 |
| AMS1117 | 3.3V LDO regulator | 1 |
| Optocoupler Relay Module | 1-channel 5V optocoupler relay, 250V/10A SPDT, active LOW | 1 |
| AC 110V Industrial Strobe Siren | AC 110V/120V, 110–120 dB, LED strobe + horn (kbaoele / Saladulce) | 1 |
| Green LED + 220Ω Resistor | 5mm green LED (armed/occupied indicator) | 1 |
| Red LED + 220Ω Resistor | 5mm red LED (alarm indicator) | 1 |
| Momentary Push Button | 12mm NO momentary button (officer manual reset) | 1 |
Sensors & Modules Explained
Ai-Thinker RD-03
24 GHz FMCW mmWave Radar

The RD-03 contains a complete 24 GHz FMCW radar transceiver on a 20×20 mm DIP module. Its S3KM1110 SoC runs an onboard human body detection algorithm that recognises both movement and micro-motion — the tiny chest displacement from breathing at 0.1–0.5 mm range. A sleeping prisoner is fully detected. A motionless prisoner sitting on a bunk is fully detected. This is fundamentally different from PIR, which is blind to stationary targets. OT2 outputs 3.3V when a human target is present and 0V when absent. The “target disappearing delay” (time OT2 stays HIGH after last detection) is configurable from 0 to 65535 seconds via the Ai-Thinker host computer tool — set to 5 seconds for this project.
- Frequency: 24 GHz – 24.25 GHz K-band
- Technology: FMCW radar (not PIR/IR)
- Detects: Moving AND static human presence
- Detection Range: Up to ~10.5 m (configurable)
- OT2 Output: 3.3V = present · 0V = absent
- Supply Voltage: 3.0V – 3.6V (3.3V nominal)
- Peak Current: > 200 mA (use dedicated LDO)
- Package: DIP, 2.54 mm pitch (breadboard-friendly)
For more precise detection you can also use: 60GHz C1001 mmWave Human Detection Sensor: Detects Life, Fall and sleep
Optocoupler Relay Module
5V / 250V 10A SPDT Active-LOW

A relay allows a 5V Arduino GPIO signal to control a 110V AC load with complete electrical isolation. The optocoupler type uses an LED-phototransistor pair (PC817) between the control side and the AC switching side — no electrical path exists between the low-voltage Arduino circuitry and the dangerous 110V siren circuit. The module is active LOW: writing LOW to the IN pin energises the coil and closes the Normally Open (NO) contact, completing the AC siren circuit. A flyback diode across the relay coil is integrated on the module, protecting the transistor driver from inductive back-EMF.
- Control Voltage: 5V DC
- Trigger Logic: Active LOW
- AC Rating: 250V / 10A (siren draws ~0.5–1A)
- Isolation: PC817 optocoupler (5000V isolation)
- Flyback Diode: Yes (onboard)
- Status LED: Yes (shows relay state)
AC Industrial Strobe Siren
AC 110V or 220v / 110–120 dB + LED Strobe

Not a buzzer — a mains-powered industrial alarm device used in factories and warehouses. Combines an electronic horn (110–120 dB, fully audible across a prison wing) with a high-intensity LED strobe (90–150 flashes/minute). Completely off until the relay closes. Connects to the relay’s NO and COM contacts — the relay inserts itself in the Live wire feeding the siren, while Neutral is wired directly. Look for IP55 or IP65 rated units for use in humid or wet correctional facility environments.
- Supply: AC 110V / 120V or 220v (check device description)
- Sound: 110–120 dB
- Strobe: LED, 90–150 flashes/min
- Protection: IP55 / IP65
- Current Draw: ~0.5–1.0 A(depends on model)
Circuit Connections
Connect all the required components as shown in the below circuit diagram. Work through these tables in order. Power supply first, then sensor, then relay, then indicators. AC wiring is always done last with the mains supply physically disconnected.

Build a Reliable Presence Detection System with PCBWay—Custom PCBs & 3D-Printed Enclosures for Your mmWave & Arduino Project
Create a non-intrusive, privacy-respecting monitor for occupancy detection using your mmWave presence sensor and Arduino. PCBWay’s integrated manufacturing services help you turn your prototype into a robust, tamper-resistant alert system suitable for secure environments.
Custom PCB Manufacturing for Sensitive mmWave Processing:
PCBWay manufactures high-quality custom PCBs that integrate your RD-03 or similar mmWave sensor, Arduino, buzzer, LED indicators, and power management onto a single compact board. Their 4-layer PCBs feature controlled impedance traces, proper grounding, and noise isolation to preserve the sensor’s micro-Doppler signals for reliable presence detection. Choose professional SMT assembly to receive a fully populated, tested board ready for deployment.
Secure 3D-Printed Enclosures:
Design and order a tamper-resistant enclosure through PCBWay’s advanced 3D printing service. Create a durable housing using impact-resistant ABS or nylon, featuring concealed mounting points, a shielded sensor window, and secure screw bosses. Add internal PCB locking mechanisms and cable strain relief—all printed with precision to ensure long-term reliability in demanding environments.
Why PCBWay Delivers Professional Results:
- Reliable Detection: Precision PCB design ensures consistent mmWave signal processing
- Tamper-Resistant Build: Custom enclosures protect electronics and deter interference
- Seamless Manufacturing: Perfect PCB-to-enclosure fit from one trusted source
Build Your Alert System Today:
Upload your PCB design and enclosure model for instant quotes, DFM feedback, and professional manufacturing only at PCBWAY.COM
Program Code
Complete, upload-ready Arduino sketch. No external libraries required — standard Arduino core only. Upload via Arduino IDE 2.x with Board set to “Arduino Nano or UNO” which ever you are using.
/*
═══════════════════════════════════════════════════════════
Prison Cell Monitoring & Alerting System
═══════════════════════════════════════════════════════════
*/
// ── Pin Assignments ────────────────────────────────────────────
const int OT2_PIN = A0; // RD-03 OT2 → 100Ω → here (+10kΩ pull-up to 5V)
const int RELAY_PIN = 8; // Relay IN (active LOW: LOW = siren ON)
const int RESET_PIN = 7; // Manual reset button (INPUT_PULLUP)
const int LED_GREEN = 13; // Green LED: armed / cell occupied
const int LED_RED = 12; // Red LED: alarm active
// ── OT2 Voltage Threshold (Software-Only Level Tolerance) ──────
// RD-03 OT2 HIGH = 3.3V = analogRead ≈ 675
// RD-03 OT2 LOW = 0.0V = analogRead ≈ 0
// Threshold at 600 = ~2.93V — gives generous noise margin
// Person PRESENT : analogRead(A0) >= OT2_THRESHOLD
// Cell EMPTY : analogRead(A0) < OT2_THRESHOLD
const int OT2_THRESHOLD = 600;
// ── Timing Constants ───────────────────────────────────────────
const unsigned long BOOT_MS = 5000UL; // #3: RD-03 boot stabilisation
const unsigned long DEBOUNCE_MS = 10000UL; // #4: 10s software absence debounce
const unsigned long BTN_DB_MS = 50UL; // Button debounce
const unsigned long POLL_MS = 100UL; // Main loop poll interval (10 Hz)
// ── System States ──────────────────────────────────────────────
enum class State : uint8_t {
BOOT, // Waiting for RD-03 boot delay
ARMING, // Waiting for initial presence confirmation (Fix #5)
MONITORING, // Cell occupied — all clear
DEBOUNCE, // OT2 dropped LOW — software confirmation timer (Fix #4)
ALARM // Confirmed empty — siren firing, latched (Fix #6)
};
State sysState = State::BOOT;
unsigned long tBoot = 0;
unsigned long tAbsence = 0;
unsigned long tLastBtn = 0;
unsigned long tLastPoll = 0;
// ═══════════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════════
// Read OT2 via ADC — software-only 3.3V tolerance (Fix #1)
bool personPresent() {
int raw = analogRead(OT2_PIN);
return (raw >= OT2_THRESHOLD); // true = HIGH = person in cell
}
void sirenOn() { digitalWrite(RELAY_PIN, LOW); } // Active-LOW relay ON
void sirenOff() { digitalWrite(RELAY_PIN, HIGH); } // Relay OFF = siren silent
void leds(bool green, bool red) {
digitalWrite(LED_GREEN, green ? HIGH : LOW);
digitalWrite(LED_RED, red ? HIGH : LOW);
}
bool resetPressed() {
if (digitalRead(RESET_PIN) == LOW) {
unsigned long now = millis();
if (now - tLastBtn > BTN_DB_MS) { tLastBtn = now; return true; }
}
return false;
}
void log(const char* msg) {
Serial.print("[T+"); Serial.print(millis()/1000); Serial.print("s] "); Serial.println(msg);
}
// ═══════════════════════════════════════════════════════════════
// SETUP
// ═══════════════════════════════════════════════════════════════
void setup() {
Serial.begin(9600);
// OT2_PIN is A0 — analogRead handles ADC; no pinMode needed for analog inputs
// (Arduino's analog pins are in INPUT mode by default)
pinMode(RELAY_PIN, OUTPUT);
pinMode(RESET_PIN, INPUT_PULLUP);
pinMode(LED_GREEN, OUTPUT);
pinMode(LED_RED, OUTPUT);
// CRITICAL: ensure relay is OFF before anything else
sirenOff();
leds(false, false);
tBoot = millis();
sysState = State::BOOT;
log("BOOT: Waiting 5s for RD-03 to stabilise...");
// Fast dual-blink during boot to signal power-on
for (int i = 0; i < 5; i++) {
leds(true, true); delay(300);
leds(false,false); delay(700);
}
}
// ═══════════════════════════════════════════════════════════════
// MAIN LOOP — Non-blocking millis() state machine
// ═══════════════════════════════════════════════════════════════
void loop() {
unsigned long now = millis();
// Throttle polling to POLL_MS interval (non-blocking)
if (now - tLastPoll < POLL_MS) return;
tLastPoll = now;
switch (sysState) {
// ─────────────────────────────────────────────────────────────
// BOOT: wait for RD-03 DSP to initialise
// ─────────────────────────────────────────────────────────────
case State::BOOT:
if (now - tBoot >= BOOT_MS) {
sysState = State::ARMING;
leds(false, false);
log("ARMING: Waiting for prisoner to be confirmed present...");
}
break;
// ─────────────────────────────────────────────────────────────
// ARMING: wait for first confirmed presence before going live
// prevents alarm when system starts with empty cell
// ─────────────────────────────────────────────────────────────
case State::ARMING: {
// Slow green blink = waiting to arm
bool blk = ((now / 600) % 2 == 0);
leds(blk, false);
if (personPresent()) {
sysState = State::MONITORING;
leds(true, false);
log("ARMED: Prisoner confirmed. Cell monitoring active.");
}
break;
}
// ─────────────────────────────────────────────────────────────
// MONITORING: cell occupied, all clear
// ─────────────────────────────────────────────────────────────
case State::MONITORING:
sirenOff();
leds(true, false);
if (!personPresent()) {
tAbsence = now;
sysState = State::DEBOUNCE;
log("DEBOUNCE: OT2 LOW — 10s confirmation timer started...");
}
break;
// ─────────────────────────────────────────────────────────────
// DEBOUNCE: sustained absence check
// ─────────────────────────────────────────────────────────────
case State::DEBOUNCE: {
// Alternating LED blink signals "checking absence"
bool blk = ((now / 300) % 2 == 0);
leds(blk, !blk);
if (personPresent()) {
// Presence returned — abort, back to monitoring
sysState = State::MONITORING;
leds(true, false);
log("MONITORING: Presence restored — no alarm.");
} else if (now - tAbsence >= DEBOUNCE_MS) {
// Absence confirmed for full 10 seconds — ALARM (Fix #4)
sysState = State::ALARM;
log("ALARM: Sustained absence — activating AC siren!");
}
break;
}
// ─────────────────────────────────────────────────────────────
// ALARM: latched — only officer reset clears it
// ─────────────────────────────────────────────────────────────
case State::ALARM:
sirenOn();
leds(false, true);
// Presence returning does NOT clear alarm
if (resetPressed()) {
sirenOff();
sysState = State::ARMING;
leds(false, false);
log("RESET: Alarm acknowledged by officer. Re-arming...");
}
break;
}
}
Code Explanation

The sketch is a non-blocking finite state machine with five states managed through a switch-case structure. Every timing operation uses millis() — never delay() — ensuring the loop runs continuously at 10 Hz regardless of what state the system is in.
OT2_THRESHOLD and analogRead() — The Core Design Decision
The constant OT2_THRESHOLD = 600 is the heart of the software-only level tolerance solution. Arduino’s 10-bit ADC maps 0V to 0 and 5V to 1023. A 3.3V signal from the RD-03 OT2 pin maps to 3.3/5.0 × 1023 = approximately 675. By setting the threshold at 600 rather than at the ATmega328P’s official VIH minimum (3.0V = 614), we get 75 additional ADC counts of margin above the threshold. More importantly, the gap between a genuine HIGH reading (~675) and the threshold (600) is 75 counts (~0.37V) — and the gap between a genuine LOW reading (~0) and the threshold is 600 counts (2.93V). False positives from noise are essentially impossible; false negatives are impossible at normal signal levels.
personPresent() — Clean Abstraction
This single function encapsulates all OT2 reading logic. It calls analogRead(A0), compares against OT2_THRESHOLD, and returns a boolean. Every other part of the code simply asks “is a person present?” without knowing or caring about voltage levels, ADC values, or thresholds. If you ever recalibrate the threshold, you change one line. If you add averaging for noise (e.g., take 3 readings and average them), you change one function. This is correct abstraction for safety-critical code.
setup() — Safe Initialisation Sequence
The relay pin is set OUTPUT and immediately driven HIGH (sirenOff()) before any other code executes. This is non-negotiable. The ATmega328P’s digital pins are in a high-impedance INPUT state at power-on, but the relay module may interpret this as an intermediate voltage and briefly energise. Explicitly driving HIGH on the first executable line guarantees the relay coil is de-energised from the first microsecond of operation. The 5-second boot delay that follows is implemented as a blocking blink loop — acceptable in setup() since no other state machine logic needs to run yet.
STATE_BOOT — Timed Delay with Visual Confirmation
The boot delay is measured by comparing millis() against tBoot, which was captured before the blocking blink loop in setup(). This means the actual boot delay is 5 seconds of millis() time from power-on, regardless of how long the blink loop took — correct behaviour.
STATE_ARMING — Conditional Arming
The arming state uses the modulo of millis()/600 to create a 1.2-second blink cycle on the green LED without any delay() call. The system stays in ARMING indefinitely — there is no timeout. This is intentional. An officer must physically confirm the prisoner is inside (OT2 goes HIGH) before the system arms. If for any reason OT2 never reads HIGH during arming (sensor misconfiguration, wrong mounting angle), the system will not silently arm and start generating false alarms — it will stay in the slow-blink arming state, visibly signalling to maintenance staff that something needs attention.
STATE_MONITORING — High-Frequency Sentinel
The siren is explicitly turned off at the top of every monitoring iteration. This ensures that if the system re-enters MONITORING from an alarm reset, the relay is guaranteed off — there is no reliance on the alarm-exit path to have correctly de-energised the relay. Defensive programming for safety-critical state machines. The single if (!personPresent()) check is all that’s needed; the simplicity is intentional.
STATE_DEBOUNCE — The False-Alarm Firewall
The alternating green/red LED blink at 300ms intervals is a deliberate visual signal — it looks different from every other LED state in the system. Anyone who walks past the panel and sees the alternating blink pattern knows immediately that a potential absence has been detected and is being verified. This is operationally important: guards can investigate during the 10-second window rather than waiting for the full alarm to fire.
STATE_ALARM — The Latched, Officer-Acknowledged Response
The sirenOn() call at the top of every alarm iteration (not just on entry) means the relay is re-asserted every 100ms. This makes the alarm resistant to brief supply glitches or capacitive coupling issues that might momentarily raise the relay control pin. The resetPressed() function implements proper button debounce using millis() — 50ms is enough to filter all mechanical bounce while remaining responsive to a genuine press.
Polling Throttle — tLastPoll
The if (now - tLastPoll < POLL_MS) return; guard at the top of the loop is the most professional design element in the sketch. It throttles all state machine logic to exactly 10 Hz while allowing the loop() function to return immediately (giving the Arduino’s hardware interrupt handlers and other background tasks full access to CPU cycles). This is the correct alternative to putting a delay(100) at the bottom of the loop — which would block the reset button check and any future hardware interrupt responses.
Applications
The core architecture mmWave absence detection, optocoupler relay, latched alarm with manual reset is directly transferable to any occupancy-critical monitoring scenario.

Correctional Cells
Primary use case. One Arduino node per high-security cell. Each relay channel can also drive a zone indicator light at the central guard station.
Psychiatric Wards
Patient monitoring without cameras. Absence from an assigned room triggers staff alert. No privacy invasion, no human monitoring required.
Industrial Safety Zones
Verify a worker has evacuated a hazardous area before a machine cycle begins. Person still present after timeout → alarm halts the process.
Server Room / Vault
Invert the logic: alarm when someone IS present after hours. Same hardware, one line of code changed — presence triggers alarm instead of absence.
Related project here: Advanced Theft Protection Alarm Using Arduino [100% Accuracy]
School Isolation Rooms
Indirect supervision of students in isolation without requiring constant guard presence. Absence from the room triggers an immediate staff alert.
Elderly Care
If an elderly resident has not been detected in their room beyond a configurable time window, this system can alert without any camera or wearable device.
Guard Post Monitoring
Verify a security guard is present at their assigned post. Extended absence triggers a supervisor alert, detecting sleeping, abandonment, or incapacitation.
Lab Safety
Ensure a technician is present during hazardous processes. Their unexpected absence triggers an automated process shutdown and audible alert.
Scaling to Multi-Cell Deployment
A single Arduino Uno has 6 analog inputs (A0-A5) and ample digital output pins, supporting up to 6 independent cells per node – each with its own RD-03 OT2 input, relay output, and status LEDs. A 4-cell deployment would use A0-A3 for sensors and D5–D8 for relay channels, with independent alarm states per cell.
Conclusion
The Prison Cell Monitoring and Alerting System documented here is not a demonstration or a simplified tutorial version — it is a properly analyzed, properly fixed, deployable security system. Every component has a documented reason. Every failure mode has been anticipated and mitigated. Every design decision is defensible.
The choice to interface the RD-03 exclusively through its OT2 GPIO pin, read via analogRead(), is the most important decision in this project. It eliminates an entire category of failure — UART framing errors, baud rate mismatches, partial packet reception, parsing bugs — by reducing the interface to a single voltage measurement. The sensor’s internal DSP handles all the complexity of 24 GHz radar signal processing; Arduino only has to answer the question “is the ADC reading above 600?” The power and simplicity of that architecture should not be underestimated.
The dual-layer debounce (5-second sensor-level hold plus 10-second software timer) and the latched alarm requiring officer reset are what make this a real security system rather than a science fair project. A system that generates false alarms is worse than no system at all — operators stop responding to it within days. A system whose alarm can be cleared without human acknowledgement is a system that can be exploited. Both of those failure modes are explicitly designed out of this build.
Before deployment, validate thoroughly: confirm OT2 reads ≥ 675 (analogRead) when the prisoner is in the cell from multiple positions, confirm the alarm fires within 20 seconds of the cell being vacated, and confirm the siren is audible from the guard station with the door closed. If you like this project please share with the persons who are interested and make a try. If you have any questions please write it in a comment below. Also please subscribe to our YouTube channel for interesting project at : YT/@circuitschools
