The Headless Pi 5 Sensor Node: Parts and Pinout

Running a Raspberry Pi headless (without a monitor or keyboard) is the standard for embedded IoT deployments. When you need to SSH into a Raspberry Pi from a Mac to manage a remote sensor node, you are bypassing the desktop environment entirely to save RAM and CPU cycles. This guide walks through deploying a headless environmental monitor using the latest hardware, establishing a secure SSH link from macOS, and debugging the inevitable network hiccups.

Board Variant Target: All commands, pinouts, and code in this guide specifically target the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS Bookworm (64-bit, Lite). The Pi 5 uses the RP1 southbridge chip, which changes some peripheral addressing, but the primary I2C bus remains on GPIO2 and GPIO3.

Hardware Parts List

  • Compute: Raspberry Pi 5 (4GB) - Do not use the 8GB for simple sensor polling; 4GB is sufficient and runs cooler.
  • Power: Official Raspberry Pi 27W USB-C PD Power Supply. Using a standard 15W phone charger will trigger low-voltage warnings on the Pi 5 and throttle the CPU.
  • Sensor: Adafruit BME280 I2C/SPI Temperature/Humidity/Pressure Sensor (Product ID: 2652).
  • Wiring: 4x female-to-female jumper wires or a 20-pin GPIO ribbon cable.
  • Storage: 32GB SanDisk Extreme microSD (A2 rating for faster OS boot times).

Pi 5 to BME280 Pin Mapping Table

The BME280 communicates via I2C. On the Raspberry Pi 5, the default I2C bus (i2c-1) is mapped to the following physical pins on the 40-pin header.

BME280 PinPi 5 GPIO (BCM)Pi 5 Physical PinFunction
VINN/A1 or 173.3V Power
GNDN/A6, 9, 14, or 20Ground
SDAGPIO 23I2C Data
SCLGPIO 35I2C Clock

Prepping the Mac and Pi for Passwordless SSH

Before you can SSH into the Raspberry Pi from your Mac, you need to configure the OS image to allow headless access right out of the gate. The days of blindly plugging in a monitor to run sudo raspi-config are over.

Step 1: Flash with OS Customization

Use the Raspberry Pi Imager on your Mac. Select the Pi 5, choose Raspberry Pi OS Lite (64-bit), and click the gear icon (OS Customization). You must check 'Enable SSH' and select 'Use password authentication' (or inject your Mac's public key directly here). Set your hostname to env-node and configure your local WiFi SSID and password.

Step 2: Generate and Copy SSH Keys on Mac

Password authentication is a security risk for exposed nodes. Generate an Ed25519 key pair on your Mac terminal and push it to the Pi.

# Generate the key on your Mac (press Enter to accept defaults, leave passphrase empty for automated cron jobs)
ssh-keygen -t ed25519 -C "mac-to-pi-node"

# Copy the key to the Pi using mDNS (.local)
ssh-copy-id pi@env-node.local

Step 3: Connection Decision Tree

How you address the Pi on the network depends on your router's mDNS support and your physical setup. Use this decision path to pick your connection method.

ScenarioMethodCommandVerdict
Standard Home Network (Mac & Pi on same WiFi/Ethernet)mDNS Hostnamessh pi@env-node.localDefault Pick. Most reliable for local LAN. No IP tracking needed.
Router blocks mDNS / Enterprise VLANStatic IP via DHCP Reservationssh pi@192.168.1.50Use when .local fails. Reserve IP in router admin panel.
No WiFi/Ethernet available (Field deployment)USB-C Direct Ethernetssh pi@10.55.0.1Pi 5 supports USB-C gadget mode, but requires config.txt tweaks. Use only as fallback.

Deploying the BME280 Python Script via SSH

Once logged in via ssh pi@env-node.local, you need to install the I2C tools and the Adafruit CircuitPython libraries. The Pi 5 Bookworm release uses PEP 668, meaning you cannot use pip install globally without breaking system packages. You must use a virtual environment.

# Enable I2C interface via the new raspi-config non-interactive flag
sudo raspi-config nonint do_i2c 0

# Create and activate a virtual environment
python3 -m venv ~/sensor_env
source ~/sensor_env/bin/pip
pip install adafruit-circuitpython-bme280

Compilable Python Sensor Script

Save the following code as read_bme.py. This script includes explicit pin definitions via the board module and robust error handling for I2C bus lockups, which are common on long wire runs.

import time
import sys
import board
import busio
import adafruit_bme280

# Target: Raspberry Pi 5 - Default I2C (GPIO3=SCL, GPIO2=SDA)
# If using an alternate I2C bus, define explicitly: busio.I2C(board.D5, board.D6)
i2c = board.I2C()

def init_sensor():
    try:
        # BME280 default I2C address is 0x77. Adafruit breakout is 0x77.
        sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x77)
        sensor.sea_level_pressure = 1013.25
        return sensor
    except ValueError as e:
        print(f'FATAL: Sensor not found on I2C bus. Check wiring. Error: {e}', file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f'FATAL: I2C Bus locked or permission denied. Error: {e}', file=sys.stderr)
        sys.exit(1)

def main():
    sensor = init_sensor()
    print('BME280 Initialized. Polling every 10 seconds...')
    
    while True:
        try:
            temp_c = sensor.temperature
            humidity = sensor.relative_humidity
            pressure = sensor.pressure
            
            # Output formatted for easy parsing by bash scripts or MQTT
            print(f'DATA | Temp: {temp_c:.2f}C | Hum: {humidity:.1f}% | Press: {pressure:.2f}hPa')
            sys.stdout.flush()
            time.sleep(10)
            
        except OSError as e:
            # Handle transient I2C read errors without crashing the daemon
            print(f'WARN: I2C Read Error: {e}. Retrying in 5s...', file=sys.stderr)
            time.sleep(5)
            sensor = init_sensor() # Re-initialize on bus failure
        except KeyboardInterrupt:
            print('\nShutting down gracefully.')
            sys.exit(0)

if __name__ == '__main__':
    main()

Troubleshooting: Connection Refused and mDNS Failures

When you try to SSH into a Raspberry Pi from a Mac, network discovery is usually the point of failure. Here is how to diagnose the two most common exact error strings.

Error 1: The mDNS Timeout

Exact Error String: ssh: Could not resolve hostname env-node.local: nodename nor servname provided, or not known

This means your Mac's Bonjour (mDNS) service cannot find the Pi's broadcasted hostname.

  • Cause A (Most Likely): The Pi is connected to a 5GHz-only WiFi network, and your Mac is on the 2.4GHz band, with AP isolation enabled on the router.
  • Cause B: The Pi hasn't finished booting. The avahi-daemon (which handles mDNS on Linux) takes about 15-20 seconds to start after the Pi 5 boots.
  • Fix: Ping the IP address directly. Log into your router's admin panel, find the Pi's assigned IP in the DHCP client list, and use ssh pi@192.168.x.x.

Error 2: The Port Block

Exact Error String: ssh: connect to host 192.168.1.42 port 22: Connection refused

This is a TCP-level rejection. Your Mac found the IP, but the Pi slammed the door in its face.

The First 3 Things to Check When It Fails:
  1. Is the ssh file present? If you didn't use the Raspberry Pi Imager's OS customization, you must manually create an empty file named ssh (no extension) in the root of the FAT32 boot partition of the SD card before booting.
  2. Is the Pi actually on the network? Check your router's DHCP lease table. If the Pi isn't listed, it failed to connect to WiFi (check your wpa_supplicant.conf or Imager WiFi password for typos).
  3. Is your Mac on the same VLAN/Subnet? If your Mac is on a guest network or a segmented IoT VLAN that blocks local peer-to-peer traffic, the connection will be refused or time out.

Extending and Simplifying the Build

A headless node is only as good as its data pipeline. Once your SSH connection is stable and the Python script is running via systemd, you have two paths forward depending on your project scope.

How to Extend: Add MQTT Telemetry

Polling a script via SSH is fine for debugging, but terrible for production. Extend the build by installing paho-mqtt in your virtual environment. Modify the read_bme.py script to publish the DATA string to an MQTT broker (like Mosquitto running on a Home Assistant server). This allows you to completely sever the SSH connection and let the Pi push data asynchronously. Add a 10k pull-up resistor to the SDA line if your I2C wire run exceeds 30cm to prevent MQTT publish drops caused by sensor read timeouts.

How to Simplify: Ditch Python for Rust/C

If you are strictly logging to a local CSV and want to eliminate the 40MB RAM overhead of the Python virtual environment, simplify the build by compiling a lightweight C binary using the WiringPi or pigpio libraries. However, for 95% of hobbyist and prosumer IoT deployments, the Python + systemd route offers the best balance of development speed and runtime stability.

Final Recommendation: Stick to the Raspberry Pi Imager OS Customization for initial setup, use Ed25519 SSH keys for authentication, and rely on mDNS (.local) for your daily terminal access. It is the most frictionless workflow for Mac-to-Pi embedded development available today.