top of page

Deploying NOMA AI: Systemd Service and Startup Scripts

  • Writer: Anie Etor-Udofia
    Anie Etor-Udofia
  • Apr 1
  • 3 min read

The Startup Challenge

Getting NOMA AI to run reliably on the Raspberry Pi required careful configuration. The app needs:

  • Virtual environment with correct dependencies

  • GPIO access (requires root or group permissions)

  • Camera access

  • Touchscreen display configuration

  • Clean shutdown and restart

The Solution: Systemd Service

I created noma_ai.service to manage the application:

ini

[Unit]
Description=NOMA AI Skin Analysis System
After=multi-user.target

[Service]
Type=simple
User=havil
Group=havil
WorkingDirectory=/home/havil/noma_ai
ExecStart=/home/havil/noma_ai/start_noma.sh
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=noma_ai
TimeoutStopSec=30
KillMode=process

[Install]
WantedBy=multi-user.target

Key Decisions:

  • After=multi-user.target: Boot to console, not graphical (saves RAM)

  • Type=simple: No forking—the script runs in foreground

  • Restart=on-failure: Automatic recovery if the app crashes

  • RestartSec=10: Wait 10 seconds before restarting (prevents rapid failure loops)

  • KillMode=process: Only kill the main process, not child processes (cleaner shutdown)

The Startup Script: start_noma.sh

This script does the heavy lifting before launching the app:

bash

#!/bin/bash
echo "=== NOMA AI Startup ==="
date

# Navigate to app directory
cd /home/havil/noma_ai || exit 1

# Activate virtual environment
if [ -f "venv/bin/activate" ]; then
    source venv/bin/activate
    echo "Virtual environment activated"
    echo "Python: $(python --version)"
    echo "Python path: $(which python)"
else
    echo "ERROR: Virtual environment not found"
    exit 1
fi

# Clean up Qt plugin paths that might conflict
unset QT_QPA_PLATFORM_PLUGIN_PATH
unset QT_PLUGIN_PATH

# Kill any existing Python processes
echo "Cleaning up existing processes..."
pkill -f "python.*noma_app" || true
sleep 1

# Reset GPIO pins safely
echo "Resetting GPIO pins..."
if [ -d /sys/class/gpio ]; then
    for pin in 17 27 22; do
        if [ -d "/sys/class/gpio/gpio${pin}" ]; then
            echo "in" > /sys/class/gpio/gpio${pin}/direction 2>/dev/null || true
            echo "${pin}" > /sys/class/gpio/unexport 2>/dev/null || true
        fi
    done
fi

# Wait for display hardware to be ready
echo "Waiting for display..."
sleep 5

# Set eglfs environment
export QT_QPA_PLATFORM=eglfs
export QT_QPA_EGLFS_HIDECURSOR=1
export QT_QPA_EGLFS_WIDTH=800
export QT_QPA_EGLFS_HEIGHT=480
export QT_QPA_EGLFS_PHYSICAL_WIDTH=154
export QT_QPA_EGLFS_PHYSICAL_HEIGHT=86
export QT_QPA_EGLFS_FORCE888=1
export QT_AUTO_SCREEN_SCALE_FACTOR=0
export QT_SCALE_FACTOR=1
export QT_LOGGING_RULES="*.debug=false;qt.qpa.*=false"

# Run the app
echo "Starting NOMA AI application..."
python noma_app.py

# Log exit
APP_EXIT_CODE=$?
echo "NOMA AI application exited with code: $APP_EXIT_CODE"
date

# Clean up
deactivate 2>/dev/null || true
exit $APP_EXIT_CODE

Why Virtual Environment?

The Raspberry Pi has multiple Python installations (system Python 3.11, user-installed packages). Using a virtual environment:

  • Isolates dependencies (no conflicts)

  • Allows specific package versions

  • Makes deployment reproducible

GPIO Reset: Why and How

GPIO pins can stay in the wrong state if the app crashes. The script:

  1. Sets each pin to "input" mode (safe state)

  2. Unexports the pin from sysfs

  3. This ensures LEDs are off and pins are ready for next run

Display Configuration for Waveshare 5" Touchscreen

The eglfs platform settings:

  • QT_QPA_PLATFORM=eglfs: Use embedded OpenGL ES (fast, no X11 overhead)

  • QT_QPA_EGLFS_HIDECURSOR=1: No cursor visible on touchscreen

  • Physical dimensions: 154mm × 86mm (5" screen)

  • QT_QPA_EGLFS_FORCE888=1: Force 24-bit color depth

Installing and Starting the Service

bash

# Make script executable
chmod +x /home/havil/noma_ai/start_noma.sh

# Copy service file
sudo cp noma_ai.service /etc/systemd/system/

# Reload systemd
sudo systemctl daemon-reload

# Enable auto-start on boot
sudo systemctl enable noma_ai.service

# Start now
sudo systemctl start noma_ai.service

# Check status
sudo systemctl status noma_ai.service

# View logs
sudo journalctl -u noma_ai.service -f

Logging Strategy

Logs go to systemd journal:

  • Standard output → journalctl -u noma_ai.service

  • Standard error → same journal

  • Custom identifier: noma_ai (grep with -t noma_ai)

This makes debugging much easier than writing to files.

Testing the Full Stack

Without Hardware (Mock Mode):

bash

# Run manually (GPIO falls back to mock mode)
python noma_app.py

With Hardware:

bash

# Run via systemd
sudo systemctl start noma_ai.service

Lessons Learned

  1. Sleep 5 seconds: The display hardware needs time to initialize. Without this, the app starts before the screen is ready and appears blank.

  2. pkill before start: Ensures no zombie processes from previous runs.

  3. GPIO reset in script, not Python: If Python crashes, GPIO pins may stay high. Resetting them in the startup script ensures a clean slate.

  4. Unset Qt plugin paths: Qt can pick up conflicting plugin paths from the environment. Unsetting them forces Qt to use the defaults.

  5. Virtual environment: The Raspberry Pi's system Python has many packages installed. Using a virtual environment ensures the app uses exactly the dependencies it needs.

Comments


bottom of page