Deploying NOMA AI: Systemd Service and Startup Scripts
- 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.targetKey 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_CODEWhy 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:
Sets each pin to "input" mode (safe state)
Unexports the pin from sysfs
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 -fLogging 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.pyWith Hardware:
bash
# Run via systemd
sudo systemctl start noma_ai.serviceLessons Learned
Sleep 5 seconds: The display hardware needs time to initialize. Without this, the app starts before the screen is ready and appears blank.
pkill before start: Ensures no zombie processes from previous runs.
GPIO reset in script, not Python: If Python crashes, GPIO pins may stay high. Resetting them in the startup script ensures a clean slate.
Unset Qt plugin paths: Qt can pick up conflicting plugin paths from the environment. Unsetting them forces Qt to use the defaults.
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