Last active
August 23, 2025 16:45
-
-
Save zitterbewegung/ccdbf8ac35c00bbab06b64ced6bb7ff7 to your computer and use it in GitHub Desktop.
Psychic Paper Quickstart.
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
| # 1) Copy your files to the Pi (adjust path/host as needed) | |
| # Put setup-inky-app.sh, main.py, requirements-2.txt in the same folder. | |
| # From your local machine: | |
| # scp setup-inky-app.sh main.py requirements-2.txt [email protected]:/home/pi/ | |
| # 2) SSH into the Pi: | |
| # ssh [email protected] | |
| # 3) Run the setup (as root): | |
| sudo bash ./setup-inky-app.sh | |
| # 4) (Optional) Reboot to apply SPI/I2C group membership: | |
| sudo reboot | |
| # Useful service commands | |
| # Check logs | |
| journalctl -u inky-app.service -e -f | |
| # Restart after editing main.py | |
| sudo systemctl restart inky-app.service | |
| # Stop / Disable | |
| sudo systemctl stop inky-app.service | |
| sudo systemctl disable inky-app.service |
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 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) |
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}" | |
| # Use uv-managed Python EXACTLY 3.10.14 | |
| UV_BIN="/usr/local/bin/uv" | |
| PY_SPEC="3.10.14" # hard pin so we never slide to 3.11 | |
| # ============================================ | |
| # Root check | |
| # ============================================ | |
| if [[ "${EUID}" -ne 0 ]]; then | |
| echo "Run as root: sudo bash ./deploy-inky-app-uv-py310.sh" | |
| exit 1 | |
| fi | |
| # ============================================ | |
| # App dir and main.py | |
| # ============================================ | |
| install -d -o "${APP_USER}" -g "${APP_GROUP}" "${APP_DIR}" | |
| cat > "${APP_DIR}/main.py" <<'__MAIN_PY__' | |
| #!/usr/bin/env python3 | |
| from fastapi import FastAPI | |
| from fastapi.middleware.cors import CORSMiddleware | |
| import uvicorn, os, time | |
| # If using an Inky display, uncomment and adapt: | |
| # from inky.auto import auto | |
| # inky = auto() | |
| app = FastAPI(title="Inky Service") | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], allow_credentials=True, | |
| allow_methods=["*"], allow_headers=["*"], | |
| ) | |
| @app.get("/") | |
| def root(): | |
| # Example: | |
| # inky.set_border(inky.BLACK); inky.show() | |
| return {"status": "ok", "msg": "Inky service running"} | |
| @app.get("/healthz") | |
| def healthz(): | |
| return {"ok": True, "ts": time.time()} | |
| if __name__ == "__main__": | |
| host = os.environ.get("HOST","0.0.0.0") | |
| port = int(os.environ.get("PORT","8000")) | |
| uvicorn.run("main:app", host=host, port=port, reload=False) | |
| __MAIN_PY__ | |
| chown "${APP_USER}:${APP_GROUP}" "${APP_DIR}/main.py" | |
| chmod 0644 "${APP_DIR}/main.py" | |
| # ============================================ | |
| # OS deps (for Pillow/inky + SPI/I2C tools) | |
| # ============================================ | |
| export DEBIAN_FRONTEND=noninteractive | |
| apt-get update | |
| apt-get install -y \ | |
| curl ca-certificates git \ | |
| libopenjp2-7 libtiff5 libjpeg-dev zlib1g-dev libfreetype6-dev \ | |
| liblcms2-dev libwebp-dev libatlas-base-dev \ | |
| i2c-tools raspi-config | |
| # ============================================ | |
| # 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" || e_ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment