#!/usr/bin/env bash # ============================================================================= # Particulate-matter (PMS7003) module v2 (Raspberry Pi 4 Model B) install script # Target OS: Raspberry Pi OS Bookworm (64-bit, aarch64) # # Air-gapped support: # - If debs/ or wheelhouse/ is present, automatically switch to "offline mode". # - In offline mode, never access the network. If the bundle is only partial, # do not silently fall back to online; stop with a clear error (air-gapped rule). Pre- # download is done on the build machine via vendor.sh. # - Can be forced via the OFFLINE=1/0 environment variable. # - Log file owner: SERVICE_USER (default pi). # # This script also enables the UART (enable_uart=1) and DISABLES the serial login console # (so the PMS7003 has exclusive use of /dev/serial0), and adds the user to the dialout group. # ============================================================================= set -euo pipefail cd "$(dirname "$0")" SERVICE_USER="${SERVICE_USER:-pi}" echo "==========================================" echo " Particulate-matter (PMS7003) module v2 (Pi 4) install" echo "==========================================" # 0) Board model notice (not enforced) if [ -r /proc/device-tree/model ]; then MODEL="$(tr -d '\0' < /proc/device-tree/model)" echo "[INFO] Board: ${MODEL}" case "${MODEL}" in *"Raspberry Pi 4"*) : ;; *) echo "[WARN] This folder is for Raspberry Pi 4 Model B only (you may continue anyway)." ;; esac fi # Offline mode detection OFFLINE="${OFFLINE:-}" if [ -z "${OFFLINE}" ]; then if compgen -G "debs/*.deb" >/dev/null || [ -d wheelhouse ]; then OFFLINE=1 echo "[INFO] Offline bundle detected -> air-gapped offline install mode (no network)" else OFFLINE=0 echo "[INFO] No offline bundle -> online install mode (internet required)" fi fi # 1) System packages: requests + venv echo "[1/4] Installing system packages..." if [ "${OFFLINE}" = "1" ]; then if ! compgen -G "debs/*.deb" >/dev/null; then echo "[ERROR] Offline mode but debs/ is empty. Place the vendor.sh output (debs/) in this folder and re-run." >&2 exit 1 fi echo " Offline .deb install (apt-get --no-download - dependency ordering/resolution automatic, no network)" # --no-install-recommends: vendor.sh does not bundle Recommends (--no-recommends), so match that. sudo apt-get install -y --no-install-recommends --no-download ./debs/*.deb else echo " Online apt install (internet required)" sudo apt-get update sudo apt-get install -y python3-requests python3-venv fi # 2) Enable UART + disable the serial login console echo "[2/4] Enabling UART (and disabling the serial console)..." # Boot config path: Bookworm=/boot/firmware/config.txt, older=/boot/config.txt BOOT_CFG="" for c in /boot/firmware/config.txt /boot/config.txt; do if [ -f "$c" ]; then BOOT_CFG="$c"; break; fi done if [ -n "${BOOT_CFG}" ]; then if ! grep -q "^enable_uart=1" "${BOOT_CFG}"; then echo " Adding enable_uart=1 to ${BOOT_CFG} (applied after reboot)" echo "enable_uart=1" | sudo tee -a "${BOOT_CFG}" >/dev/null NEED_REBOOT=1 else echo " enable_uart=1 already set" fi else echo "[WARN] config.txt not found - enable UART manually via raspi-config" fi # Disable the serial login console so /dev/serial0 is free for the sensor. # Bookworm=/boot/firmware/cmdline.txt, older=/boot/cmdline.txt CMDLINE="" for c in /boot/firmware/cmdline.txt /boot/cmdline.txt; do if [ -f "$c" ]; then CMDLINE="$c"; break; fi done if [ -n "${CMDLINE}" ] && grep -q "console=serial0" "${CMDLINE}"; then echo " Removing 'console=serial0,115200' from ${CMDLINE} (frees the UART for PMS7003)" sudo sed -i 's/console=serial0,[0-9]* //' "${CMDLINE}" NEED_REBOOT=1 fi # Disable the getty on the primary UART (harmless if already masked). sudo systemctl disable --now serial-getty@ttyS0.service 2>/dev/null || true sudo systemctl disable --now serial-getty@ttyAMA0.service 2>/dev/null || true # Add the service user to the dialout group (/dev/serial0 access permission) if getent group dialout >/dev/null 2>&1; then sudo usermod -aG dialout "${SERVICE_USER}" || echo "[WARN] usermod failed - check SERVICE_USER=${SERVICE_USER}" fi # 3) Python venv + requests + pyserial echo "[3/4] Setting up Python virtual environment..." if [ ! -d venv ]; then python3 -m venv --system-site-packages venv echo " venv created" else echo " venv already exists - skipping" fi # shellcheck disable=SC1091 source venv/bin/activate if [ "${OFFLINE}" = "1" ]; then if [ ! -d wheelhouse ]; then echo "[ERROR] Offline mode but wheelhouse/ is missing. Place the vendor.sh output (wheelhouse/) and re-run." >&2 exit 1 fi echo " Offline wheel install (--no-index - no network)" pip install --no-index --find-links=wheelhouse -r requirements.txt else echo " Online pip install (internet required)" pip install --upgrade pip pip install -r requirements.txt fi echo " Packages installed" # 4) Log file (least privilege: owner-only write) echo "[4/4] Creating log file..." sudo mkdir -p /var/log sudo touch /var/log/pms7003_sensor.log if ! sudo chown "${SERVICE_USER}:${SERVICE_USER}" /var/log/pms7003_sensor.log; then echo "[WARN] chown failed - check whether SERVICE_USER=${SERVICE_USER} exists" fi sudo chmod 640 /var/log/pms7003_sensor.log # Lock config_local.py immediately (contains API_KEY - prevent world-readable until harden) # Hardening (harden_pi.sh) tightens it again later, but to avoid exposure even in # that window, lock it to 600 right at install time. Protected regardless of harden. if [ -f config_local.py ]; then chmod 600 config_local.py sudo chown "${SERVICE_USER}:${SERVICE_USER}" config_local.py 2>/dev/null || true echo " config_local.py set to permission 600 (owner ${SERVICE_USER})" fi # config_local notice (fail closed - if missing, execution stops immediately) if [ ! -f config_local.py ]; then echo "" echo "[!] config_local.py is missing. Run the following, then fill in real values:" echo " cp config_local.example.py config_local.py" fi echo "" echo "==========================================" echo " Install complete! (OFFLINE=${OFFLINE})" echo "==========================================" if [ "${NEED_REBOOT:-0}" = "1" ]; then echo " [!] UART was just enabled / serial console disabled -> reboot to apply: 'sudo reboot'." fi echo " Port check: ls -l /dev/serial0 (should symlink to ttyS0 or ttyAMA0)" echo " Run: source venv/bin/activate && python3 pms7003_sensor.py" echo " Service: see the systemd section in README.md" echo ""