from fastapi import FastAPI, File, Form, UploadFile, HTTPException, Query from fastapi.middleware.cors import CORSMiddleware import subprocess import threading import time import os import json import uuid import hashlib from pathlib import Path from pydantic import BaseModel import RPi.GPIO as GPIO app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) INCOMING_DIR = Path("/tmp/quibot-audio/incoming") LOCKED_DIR = Path("/tmp/quibot-audio/locked") PROCESSED_DIR = Path("/tmp/quibot-audio/processed") INCOMING_DIR.mkdir(parents=True, exist_ok=True) LOCKED_DIR.mkdir(parents=True, exist_ok=True) PROCESSED_DIR.mkdir(parents=True, exist_ok=True) # ------------------------- # GPIO SETUP # ------------------------- STEP = 23 DIR = 24 EN = 25 GPIO.setmode(GPIO.BCM) GPIO.setup(STEP, GPIO.OUT) GPIO.setup(DIR, GPIO.OUT) GPIO.setup(EN, GPIO.OUT) GPIO.output(EN, GPIO.LOW) motor_thread = None def step_motor(steps, direction, delay=0.001): GPIO.output(DIR, direction) for _ in range(steps): GPIO.output(STEP, GPIO.HIGH) time.sleep(delay) GPIO.output(STEP, GPIO.LOW) time.sleep(delay) def motor_step(dir): dir_pin = GPIO.HIGH if dir == "forward" else GPIO.LOW time.sleep(0.02) # small delay before starting print("Motor running...") step_motor(200, dir_pin, 0.001) # ------------------------- # SAFE COMMAND WHITELIST # ------------------------- COMMANDS = { "restart_nginx": ["sudo", "systemctl", "restart", "nginx"], "uptime": ["uptime"], "update": ["sudo", "apt", "update"] } # ------------------------- # API ENDPOINTS # ------------------------- @app.post("/run") def run_task(task: str, token: str): if token != "MY_SECRET_TOKEN": raise HTTPException(status_code=403, detail="Unauthorized") if task not in COMMANDS: raise HTTPException(status_code=400, detail="Invalid task") try: result = subprocess.check_output(COMMANDS[task], text=True) return {"output": result} except subprocess.CalledProcessError as e: return {"error": e.output} @app.post("/motor/step/forward") def start_motor(token: str): global motor_thread if token != "MY_SECRET_TOKEN": raise HTTPException(status_code=403, detail="Unauthorized") motor_thread = threading.Thread(target=motor_step, args=("forward",), daemon=True) motor_thread.start() return {"status": "motor started"} @app.post("/motor/step/backwards") def start_motor(token: str): global motor_thread if token != "MY_SECRET_TOKEN": raise HTTPException(status_code=403, detail="Unauthorized") motor_thread = threading.Thread(target=motor_step, args=("backwards",), daemon=True) motor_thread.start() return {"status": "motor started"} @app.post("/motor/stop") def stop_motor(token: str): if token != "MY_SECRET_TOKEN": raise HTTPException(status_code=403, detail="Unauthorized") GPIO.output(EN, GPIO.HIGH) # disable driver return {"status": "motor stopped"} @app.post("/audio/upload") async def upload_audio(file: UploadFile = File(...), format: str = "wav"): raw_content = await file.read() checksum = hashlib.sha256(raw_content).hexdigest()[:16] filename = f"{checksum[:10]}-{uuid.uuid4().hex[:8]}.wav" filepath = INCOMING_DIR / filename filepath.write_bytes(raw_content) return {"status": "received", "filename": str(filepath), "lock_url": f"/audio/lock/{filepath.name}"} @app.get("/audio/incoming") def list_incoming(): files = [] for f in sorted(INCOMING_DIR.iterdir()): meta = f.stat() files.append({ "filename": f.name, "size_bytes": meta.st_size, "modified_iso": time.ctime(meta.st_mtime), }) return {"count": len(files), "files": files} @app.post("/audio/lock/{filename}") def lock_audio(filename: str): src = INCOMING_DIR / filename dst = LOCKED_DIR / filename if not src.exists(): raise HTTPException(status_code=404, detail=f"File {filename} not found") if dst.exists(): return {"status": "already_locked", "filename": filename} os.rename(str(src), str(dst)) return {"status": "locked", "filename": filename} @app.post("/audio/unlock/{filename}") def unlock_audio(filename: str): src = LOCKED_DIR / filename dst = INCOMING_DIR / filename if not src.exists(): raise HTTPException(status_code=404, detail=f"File {filename} not found") os.rename(str(src), str(dst)) return {"status": "unlocked", "filename": filename} @app.post("/audio/cancel/{filename}") def cancel_audio(filename: str): src = LOCKED_DIR / filename dst = INCOMING_DIR / filename if not src.exists(): raise HTTPException(status_code=404, detail=f"File {filename} not found") os.rename(str(src), str(dst)) return {"status": "cancelled", "filename": filename} @app.post("/audio/process/{filename}") def process_audio(filename: str): locked = LOCKED_DIR / filename processed = PROCESSED_DIR / filename if not locked.exists(): raise HTTPException(status_code=404, detail=f"File {filename} not found") os.rename(str(locked), str(processed)) return {"status": "processed", "filename": filename} @app.on_event("shutdown") def shutdown(): global motor_running motor_running = False GPIO.output(EN, GPIO.HIGH) GPIO.cleanup()