Last active
August 23, 2025 20:18
-
-
Save zitterbewegung/7a62f18ca6c98cc4f036fc0b819bf060 to your computer and use it in GitHub Desktop.
Psychic Paper Install
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # ============================================ | |
| # Config | |
| # ============================================ | |
| APP_USER="${APP_USER:-${SUDO_USER:-pi}}" | |
| APP_GROUP="${APP_GROUP:-${APP_USER}}" | |
| HOME_DIR="$(getent passwd "${APP_USER}" | cut -d: -f6)" | |
| APP_DIR="${APP_DIR:-${HOME_DIR}/inky-app}" | |
| VENV_DIR="${VENV_DIR:-${APP_DIR}/.venv}" | |
| SERVICE_NAME="${SERVICE_NAME:-inky-app.service}" | |
| PY_BIN="${PY_BIN:-python3}" | |
| PIP_BIN="${PIP_BIN:-pip3}" | |
| # Pillow 10+ removes Image.ANTIALIAS; your code uses it. | |
| # Pin Pillow to a compatible version so your code runs unchanged. | |
| PILLOW_PKG="${PILLOW_PKG:-Pillow==9.5.0}" | |
| # ============================================ | |
| # Root check | |
| # ============================================ | |
| if [[ "${EUID}" -ne 0 ]]; then | |
| echo "Run as root: sudo bash ./deploy-inky-app-system-venv.sh" | |
| exit 1 | |
| fi | |
| # ============================================ | |
| # Create app dir | |
| # ============================================ | |
| install -d -o "${APP_USER}" -g "${APP_GROUP}" "${APP_DIR}" | |
| # ============================================ | |
| # Write YOUR EXACT main.py into the app dir | |
| # ============================================ | |
| cat > "${APP_DIR}/main.py" <<'__MAIN_PY__' | |
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from PIL import Image, ExifTags | |
| from io import BytesIO | |
| import inky | |
| import time | |
| # Initialize FastAPI | |
| app = FastAPI() | |
| # Allow CORS for all origins (adjust as needed for security) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Initialize the Inky ePaper display (adjust this initialization according to your specific seven-color display) | |
| display = inky.auto() # Automatically detect your Inky display type and version | |
| def process_image(image: Image.Image): | |
| """Process the image to correct orientation and resize it for the ePaper screen.""" | |
| # Correct image orientation using EXIF data, if available | |
| try: | |
| for orientation in ExifTags.TAGS.keys(): | |
| if ExifTags.TAGS[orientation] == 'Orientation': | |
| break | |
| exif = image._getexif() | |
| if exif is not None: | |
| orientation = exif.get(orientation, None) | |
| if orientation == 3: | |
| image = image.rotate(180, expand=True) | |
| elif orientation == 6: | |
| image = image.rotate(270, expand=True) | |
| elif orientation == 8: | |
| image = image.rotate(90, expand=True) | |
| except Exception as e: | |
| print(f"EXIF orientation correction failed: {e}") | |
| # Resize the image to match the display's resolution | |
| image = image.resize(display.resolution, Image.ANTIALIAS) | |
| return image | |
| def display_image(image: Image.Image): | |
| """Function to display the image on the ePaper screen.""" | |
| try: | |
| # Simulate some processing delay for visual feedback | |
| time.sleep(2) # Optional: simulate processing delay for user feedback | |
| display.set_image(image) | |
| display.show() | |
| print("Image successfully displayed on the ePaper screen.") | |
| except Exception as e: | |
| print(f"Error displaying image: {e}") | |
| @app.post("/upload/") | |
| async def upload_image(file: UploadFile = File(...), background_tasks: BackgroundTasks = None): | |
| # Check if the uploaded file is a JPEG | |
| if not file.filename.lower().endswith(('jpeg', 'jpg')): | |
| raise HTTPException(status_code=400, detail="Invalid file format. Only JPEG images are supported.") | |
| try: | |
| # Read the image file | |
| contents = await file.read() | |
| image = Image.open(BytesIO(contents)) | |
| # Process the image without changing its color format | |
| processed_image = process_image(image) | |
| # Add the display task to the background to not block the response | |
| background_tasks.add_task(display_image, processed_image) | |
| # Return immediate response indicating that processing has started | |
| return JSONResponse(content={"filename": file.filename, "status": "Image is being processed and displayed on ePaper screen."}) | |
| except Exception as e: | |
| print(f"Error processing image: {e}") | |
| raise HTTPException(status_code=500, detail="Failed to process and display the image.") | |
| @app.get("/") | |
| async def main(): | |
| # HTML Form to upload a file | |
| content = """ | |
| <html> | |
| <head> | |
| <script> | |
| function showLoading() { | |
| document.getElementById('loading').style.display = 'block'; | |
| } | |
| </script> | |
| </head> | |
| <body> | |
| <h1>Upload a JPEG Image (640x400px)</h1> | |
| <form action="/upload/" enctype="multipart/form-data" method="post" onsubmit="showLoading()"> | |
| <input name="file" type="file" accept="image/jpeg" required> | |
| <input type="submit" value="Upload Image"> | |
| </form> | |
| <div id="loading" style="display:none;"> | |
| <p>Uploading and processing your image... Please wait!</p> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| return HTMLResponse(content=content) | |
| # Ensure this block only runs when the script is executed directly | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) | |
| __MAIN_PY__ | |
| chown "${APP_USER}:${APP_GROUP}" "${APP_DIR}/main.py" | |
| chmod 0644 "${APP_DIR}/main.py" | |
| # ============================================ | |
| # Minimal OS deps (NO libtiff) | |
| # ============================================ | |
| export DEBIAN_FRONTEND=noninteractive | |
| apt-get update | |
| apt-get install -y \ | |
| python3 python3-pip python3-venv python3-dev git \ | |
| i2c-tools raspi-config \ | |
| libjpeg62-turbo zlib1g libfreetype6 | |
| # (Intentionally NOT installing libtiff) | |
| # ============================================ | |
| # Enable SPI & I2C (idempotent) | |
| # ============================================ | |
| if command -v raspi-config >/dev/null 2>&1; then | |
| raspi-config nonint do_spi 0 || true | |
| raspi-config nonint do_i2c 0 || true | |
| raspi-config nonint do_boot_wait 0 || true | |
| fi | |
| for CFG in /boot/firmware/config.txt /boot/config.txt; do | |
| [[ -f "$CFG" ]] || continue | |
| grep -q '^dtparam=spi=on' "$CFG" || echo 'dtparam=spi=on' >> "$CFG" | |
| grep -q '^dtparam=i2c_arm=on' "$CFG" || echo 'dtparam=i2c_arm=on' >> "$CFG" | |
| done | |
| usermod -aG spi,i2c,gpio "${APP_USER}" || true | |
| # ============================================ | |
| # Python venv (system python) | |
| # ============================================ | |
| install -d -o "${APP_USER}" -g "${APP_GROUP}" "${VENV_DIR}" | |
| sudo -u "${APP_USER}" "${PY_BIN}" -m venv "${VENV_DIR}" | |
| VENV_PY="${VENV_DIR}/bin/python" | |
| VENV_PIP="${VENV_DIR}/bin/pip" | |
| "${VENV_PY}" -m pip install --upgrade pip setuptools wheel | |
| # Install only what your app needs (+ python-multipart for form uploads) | |
| "${VENV_PIP}" install --no-cache-dir inky fastapi "uvicorn[standard]" "${PILLOW_PKG}" python-multipart | |
| # Sanity check | |
| "${VENV_PY}" - <<'PY' | |
| for mod in ("inky","fastapi","uvicorn","PIL","multipart"): | |
| __import__(mod if mod!="PIL" else "PIL.Image") | |
| print("Imports OK in venv.") | |
| PY | |
| # ============================================ | |
| # systemd service | |
| # ============================================ | |
| SERVICE_PATH="/etc/systemd/system/${SERVICE_NAME}" | |
| # Stop/disable old unit if present | |
| if systemctl list-unit-files | grep -q "^${SERVICE_NAME}"; then | |
| systemctl stop "${SERVICE_NAME}" || true | |
| systemctl disable "${SERVICE_NAME}" || true | |
| fi | |
| cat > "${SERVICE_PATH}" <<EOF | |
| [Unit] | |
| Description=Inky App (main.py, system Python venv) | |
| After=network-online.target | |
| Wants=network-online.target | |
| [Service] | |
| Type=simple | |
| User=${APP_USER} | |
| Group=${APP_GROUP} | |
| WorkingDirectory=${APP_DIR} | |
| Environment=PYTHONUNBUFFERED=1 | |
| # Environment=PORT=8000 | |
| # Environment=HOST=0.0.0.0 | |
| ExecStart=${VENV_PY} ${APP_DIR}/main.py | |
| Restart=on-failure | |
| RestartSec=5 | |
| [Install] | |
| WantedBy=multi-user.target | |
| EOF | |
| chmod 0644 "${SERVICE_PATH}" | |
| systemctl daemon-reload | |
| systemctl enable --now "${SERVICE_NAME}" | |
| echo | |
| echo "============================================================" | |
| echo "Installed and started ${SERVICE_NAME}." | |
| echo "App dir : ${APP_DIR}" | |
| echo "Python : $(${VENV_PY} -c 'import sys; print(sys.version.split()[0])') (system venv)" | |
| echo "Main : ${APP_DIR}/main.py" | |
| echo | |
| echo "Log tail : journalctl -u ${SERVICE_NAME} -e -f" | |
| echo "============================================================" | |
| echo | |
| echo "If you were just added to spi/i2c groups, reboot:" | |
| echo " sudo reboot" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Here’s a single-file deploy script that uses uv to install Python 3.10.14, creates a venv from that exact interpreter, installs inky, fastapi, uvicorn[standard], enables SPI/I²C, sets up a systemd service, and writes your exact main.py (the Python code you pasted) to disk. | |
| Save as deploy-inky-app-uv-py310.sh, then run: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment