#!/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 Blueprint, Flask, jsonify, redirect, render_template_string, request, url_for
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


knock_bp = Blueprint("knock", __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('knock.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>
"""


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


@knock_bp.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('knock.index'))


@knock_bp.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})


def create_app():
    app = Flask(__name__)
    app.register_blueprint(knock_bp)
    return app


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