A Raspberry Pi cron job is the standard Linux time-based daemon (cron) executing a script at predefined intervals. In embedded hardware projects, it serves as the bridge between time-based logic and physical GPIO pins—allowing you to trigger relays, poll sensors, or log data without keeping a persistent, resource-heavy Python loop running in the background.
This guide targets the Raspberry Pi 4 Model B (4GB) running Raspberry Pi OS (Bookworm 64-bit). We will build an automated greenhouse exhaust fan controller that reads an analog temperature sensor via an ADC and triggers a 5V relay module based on a daily schedule.
The Verdict: Scheduling Framework Decision Tree
Before writing code, you must choose the right scheduling mechanism. Linux offers three primary ways to time hardware triggers. Use this decision path to select the correct tool for your architecture.
| Condition / Requirement | Recommended Tool | Why? |
|---|---|---|
| Need to run exactly at HH:MM daily and survive reboots automatically? | System Cron (crontab) |
Native to the OS, zero overhead, survives power cycles if hardware clock is synced. |
| Need to wait for network mount or run on complex calendar intervals? | Systemd Timers | Handles dependencies (e.g., After=network-online.target) natively. |
| Need sub-second precision or internal state management between runs? | Python schedule + Systemd Service |
Keeps the script in RAM; avoids the overhead of spawning a new Python interpreter every run. |
Hardware BOM and GPIO Pin Mapping
To avoid the bit-banging C-library issues common with DHT sensors on the newer Bookworm OS, this build uses an analog TMP36 temperature sensor read through an MCP3008 SPI ADC. This ensures rock-solid, native gpiozero compatibility.
Parts List
- Compute: Raspberry Pi 4 Model B (4GB RAM) - ~$55
- Power: CanaKit 5V 3.5A USB-C Power Supply - ~$15
- ADC: Microchip MCP3008 10-bit SPI ADC DIP chip - ~$4
- Sensor: Analog Devices TMP36 Analog Temperature Sensor - ~$3
- Actuator: Elegoo 4-Channel 5V Relay Module (Opto-isolated) - ~$8
- Wiring: 22 AWG solid core hook-up wire, 10kΩ decoupling capacitor (optional for ADC noise)
Pin Mapping Table (BCM Numbering)
The gpiozero MCP3008 documentation dictates strict SPI pin assignments. Do not deviate from the hardware SPI0 pins.
| Component | BCM GPIO | Physical Pin | Function |
|---|---|---|---|
| MCP3008 VDD / VREF | N/A | 1 (3.3V) | Power & Reference Voltage |
| MCP3008 CLK | GPIO 11 | 23 | SPI Clock (SCLK) |
| MCP3008 DOUT (MISO) | GPIO 9 | 21 | SPI Master In / Slave Out |
| MCP3008 DIN (MOSI) | GPIO 10 | 19 | SPI Master Out / Slave In |
| MCP3008 CS/SHDN | GPIO 8 | 24 | SPI Chip Enable 0 (CE0) |
| Relay Module IN1 | GPIO 17 | 11 | Digital Output (Active Low) |
| Relay Module VCC | N/A | 2 (5V) | Relay Coil Power |
| Common GND | N/A | 6, 9, 14, 20 | System Ground |
The Python Control Script (Bookworm Compatible)
This script uses gpiozero, which is pre-installed on Raspberry Pi OS Bookworm. It reads the ADC, calculates the temperature, and toggles the relay. It includes strict error handling to ensure the relay fails safe (turns off) if the sensor read crashes.
#!/usr/bin/env python3
import logging
import sys
from gpiozero import OutputDevice, MCP3008
from datetime import datetime
# --- Pin Definitions (BCM) ---
RELAY_PIN = 17
ADC_CHANNEL = 0 # MCP3008 Channel 0
# --- Configuration ---
TEMP_THRESHOLD_C = 28.0
LOG_FILE = '/home/pi/fan_controller.log'
# Setup Logging
logging.basicConfig(
filename=LOG_FILE,
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def get_temperature_c():
"""Reads TMP36 via MCP3008 channel 0"""
try:
# Initialize ADC on SPI0, CE0, Channel 0
adc = MCP3008(channel=ADC_CHANNEL)
voltage = adc.value * 3.3 # gpiozero returns 0.0 to 1.0
# TMP36 formula: (Voltage - 0.5V offset) * 100mV/°C
temp_c = (voltage - 0.5) * 100
return round(temp_c, 2)
except Exception as e:
logging.error(f"ADC Read Failure: {e}")
return None
def main():
# active_high=False because most relay modules are Active LOW
fan_relay = OutputDevice(RELAY_PIN, active_high=False)
temp = get_temperature_c()
if temp is None:
logging.warning("Sensor read failed. Defaulting to fan OFF for safety.")
fan_relay.off()
sys.exit(1)
logging.info(f"Current Temp: {temp}°C")
if temp > TEMP_THRESHOLD_C:
fan_relay.on()
logging.info("Threshold exceeded. Fan relay ENGAGED.")
else:
fan_relay.off()
logging.info("Temperature nominal. Fan relay DISENGAGED.")
if __name__ == "__main__":
try:
main()
except Exception as e:
logging.critical(f"Unhandled exception in cron execution: {e}")
sys.exit(1)
Configuring the Raspberry Pi Cron Job
With the script saved as /home/pi/greenhouse_fan.py and made executable (chmod +x /home/pi/greenhouse_fan.py), you must register it with the cron daemon.
- Open the crontab editor for the
piuser:crontab -e - Append the following line to run the script every day at 8:00 AM and 6:00 PM:
0 8,18 * * * /usr/bin/python3 /home/pi/greenhouse_fan.py >> /home/pi/cron_stdout.log 2>&1 - Verify the cron service is active:
sudo systemctl status cron
Crucial Detail: Cron executes with a severely restricted environment. It does not load your .bashrc. This is why we use the absolute path /usr/bin/python3 instead of just python3, and why we redirect standard error (2>&1) to a log file. If you are using a Python virtual environment, replace /usr/bin/python3 with the absolute path to your venv's python binary (e.g., /home/pi/myenv/bin/python3).
Debugging: Exact Error Strings and Ranked Causes
When a crontab job fails silently, it is almost always an environment or permission issue. Here are the exact error strings you will find in /var/log/syslog or your stdout log, ranked by frequency.
First Three Things to Check When It Fails
- Absolute Paths: Did you use
python3instead of/usr/bin/python3? Did your Python script use relative paths for log files? - Hardware Interfaces: Is SPI enabled? Run
sudo raspi-config→ Interface Options → SPI and ensure it is active. - Execution Permissions: Did you run
chmod +xon the Python script?
Ranked Error Strings
| Exact Error String | Root Cause | The Fix |
|---|---|---|
/bin/sh: 1: python3: not found |
Cron's default PATH does not include /usr/bin. |
Change python3 to /usr/bin/python3 in the crontab entry. |
PermissionError: [Errno 13] Permission denied: '/dev/spidev0.0' |
The pi user lacks hardware SPI permissions, or SPI is disabled in device tree. |
Enable SPI via raspi-config and reboot. If using a non-default user, add them to the spi group: sudo usermod -aG spi username. |
gpiozero.exc.BadPinFactory: Unable to load any default pin factory |
Running in a bare virtual environment without the RPi.GPIO or lgpio backend installed. |
Install the backend in your venv: pip install rpi-lgpio (required for Bookworm/Pi 5 compatibility). |
FileNotFoundError: [Errno 2] No such file or directory: 'fan_controller.log' |
The script uses a relative path for the log file, and cron's working directory is /home/pi (or /root if using sudo crontab). |
Use absolute paths in your Python script: /home/pi/fan_controller.log. |
Extending and Simplifying the Build
Once your base cron job is reliably toggling the relay, you will likely want to adapt the architecture based on your long-term maintenance tolerance.
How to Simplify (The No-Code Alternative)
If maintaining Linux cron tables and Python SPI dependencies feels like overkill for a simple timer, simplify by abandoning the Pi's GPIO entirely. Purchase a Shelly Plus 1PM or a Sonoff TX smart relay (~$15). Flash it with Tasmota or use the native app to set internal hardware timers. This moves the scheduling logic onto the relay's internal ESP32 chip, eliminating OS-level cron debugging, SD card corruption risks, and Python dependency rot.
How to Extend (Adding Telemetry)
If you want to track historical temperature data and relay states without polling the log file manually, extend the Python script to publish an MQTT payload.
Add import paho.mqtt.client as mqtt to the script. After the relay state is determined, publish the JSON payload {"temp_c": 28.5, "relay_state": "ON"} to a local Mosquitto broker. You can then ingest this into Home Assistant or Node-RED to build a dashboard, while keeping the cron job as the deterministic, fail-safe hardware trigger.






