#!/usr/bin/env python3
"""
Flask web UI to trigger 4 GPIO outputs on a Raspberry Pi (Bookworm/Pi 5 friendly).
- Uses gpiozero (with the LGPIO backend) instead of deprecated RPi.GPIO.
- 4 buttons on the homepage. Clicking a button pulses the mapped GPIO pin once.

Install (Raspberry Pi OS Bookworm):
  sudo apt update && sudo apt install -y python3-gpiozero python3-lgpio

Run:
  python3 app.py
Then open http://<raspberrypi-ip>:5000

NOTE:
- Ensure the downstream device shares GND with the Pi and is 3.3V‑tolerant.
- If the line drives a relay/coil or >3.3V logic, use a transistor/optocoupler level-shifter.
"""

import atexit
import threading
import time
from typing import List

from flask import Flask, render_template_string, request, redirect, url_for, jsonify
from gpiozero import OutputDevice

# ==================== Configuration ====================
# BCM pins for the 4 channels (matches your example)
RELAY_PINS: List[int] = [14, 15, 18, 23]

# Pulse duration in seconds
PULSE_DURATION: float = 0.01

# Active level for your downstream device:
#   True  -> drive pin HIGH for active pulse
#   False -> drive pin LOW  for active pulse (open-collector style boards, or active-low inputs)
ACTIVE_HIGH: bool = True
# =======================================================

# Prepare OutputDevice objects. We set initial_value=False so lines idle LOW by default (adjust as needed).
channels: List[OutputDevice] = [
    OutputDevice(pin, active_high=True, initial_value=False)
    for pin in RELAY_PINS
]

# Ensure cleanup on exit
@atexit.register
def _cleanup():
    for ch in channels:
        try:
            ch.off()
            ch.close()
        except Exception:
            pass

# Lock to prevent overlapping pulses per process
pulse_lock = threading.Lock()


def pulse(index: int, duration: float = PULSE_DURATION):
    ch = channels[index]
    with pulse_lock:
        if ACTIVE_HIGH:
            ch.on()
            time.sleep(duration)
            ch.off()
        else:
            # active-low pulse
            ch.off()  # ensure idle state
            ch.toggle()  # go active-low
            time.sleep(duration)
            ch.toggle()  # return to idle


app = Flask(__name__)

INDEX_HTML = """
<!doctype html>
<html lang=\"zh-CN\">
<head>
  <meta charset=\"utf-8\" />
  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />
  <title>GPIO 控制面板</title>
  <style>
    :root { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
    body { margin: 0; padding: 24px; background: #0b1220; color: #e6eefc; }
    .wrap { max-width: 720px; margin: 0 auto; }
    h1 { font-size: 22px; margin: 0 0 8px; }
    p { color: #9fb2d8; margin: 0 0 16px; }
    .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
    .card { background: #111a2e; border: 1px solid #1d2740; border-radius: 14px; padding: 18px; }
    .btn { display: inline-block; width: 100%; padding: 16px 18px; border-radius: 12px; border: 1px solid #2a3556; background: #132047; color: #dfe8ff; font-weight: 600; cursor: pointer; transition: transform .06s ease, background .2s ease, border-color .2s ease; }
    .btn:hover { transform: translateY(-1px); background: #172855; border-color: #33406a; }
    .btn:active { transform: translateY(0); }
    .row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
    .meta { font-size: 12px; color: #8ea3cc; }
    .footer { margin-top: 20px; font-size: 12px; color: #7e93bf; opacity: .9; }
    code { background: #0e172b; padding: 2px 6px; border-radius: 6px; border: 1px solid #1b2440; }
  </style>
</head>
<body>
  <div class=\"wrap\">
    <h1>GPIO 控制面板</h1>
    <p>点击按钮以<strong>脉冲激活</strong>对应的 GPIO（BCM）。默认脉冲：<code>{{ pulse_duration * 1000 | round(1) }}ms</code>；模式：<code>{{ 'ACTIVE_HIGH' if active_high else 'ACTIVE_LOW' }}</code></p>

    <div class=\"grid\">
      {% for idx, pin in enumerate(pins) %}
      <div class=\"card\">
        <div class=\"row\">
          <div>
            <div><strong>通道 {{ idx }}</strong></div>
            <div class=\"meta\">GPIO {{ pin }}</div>
          </div>
          <form method=\"post\" action=\"{{ url_for('activate', idx=idx) }}\">
            <button class=\"btn\" type=\"submit\">激活通道 {{ idx }}</button>
          </form>
        </div>
      </div>
      {% endfor %}
    </div>

    <div class=\"footer\">
      <div>API：POST <code>/api/activate</code> JSON: <code>{\"index\": N}</code></div>
    </div>
  </div>
</body>
</html>
"""


@app.route("/")
def index():
    return render_template_string(
        INDEX_HTML,
        pins=RELAY_PINS,
        pulse_duration=PULSE_DURATION,
        active_high=ACTIVE_HIGH,
        enumerate=enumerate,
    )


@app.route("/activate/<int:idx>", methods=["POST"])  # form action
def activate(idx: int):
    if idx < 0 or idx >= len(channels):
        return ("Invalid channel index", 400)
    threading.Thread(target=pulse, args=(idx,), daemon=True).start()
    return redirect(url_for("index"))


@app.route("/api/activate", methods=["POST"])  # programmatic JSON API
def api_activate():
    data = request.get_json(silent=True) or {}
    if "index" not in data:
        return jsonify({"ok": False, "error": "missing 'index'"}), 400
    idx = int(data["index"])
    if idx < 0 or idx >= len(channels):
        return jsonify({"ok": False, "error": "invalid index"}), 400
    threading.Thread(target=pulse, args=(idx,), daemon=True).start()
    return jsonify({"ok": True, "index": idx, "pin": RELAY_PINS[idx], "duration_sec": PULSE_DURATION, "active_high": ACTIVE_HIGH})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=False)
