Project

General

Profile

CODIGO.PY

Conexion y logica de la raspberry - rene ayca, 12/24/2025 11:13 AM

Download (11.9 KB)

 
1
from flask import Flask, jsonify, request, Response
2
import grovepi
3
import time
4
import threading
5
import json
6
import datetime
7
import os
8
import cv2
9

    
10
app = Flask(__name__)
11

    
12
@app.after_request
13
def after_request(response):
14
    response.headers.add('Access-Control-Allow-Origin', '*')
15
    response.headers.add('Access-Control-Allow-Headers', 'Content-Type')
16
    response.headers.add('Access-Control-Allow-Methods', 'GET,POST,OPTIONS')
17
    return response
18

    
19
ULTRA_COMIDA = 4
20
ULTRA_AGUA = 3
21
RELAY_AGUA = 8
22
RELAY_COMIDA = 2
23

    
24
grovepi.pinMode(ULTRA_COMIDA, "INPUT")
25
grovepi.pinMode(ULTRA_AGUA, "INPUT")
26
grovepi.pinMode(RELAY_AGUA, "OUTPUT")
27
grovepi.pinMode(RELAY_COMIDA, "OUTPUT")
28

    
29

    
30
CONFIG_DISPENSADOR = {
31
    "altura_total": 26,
32
    "niveles": {
33
        "comida": {
34
            "lleno": (0, 6),
35
            "medio": (7, 12),
36
            "poco": (13, 19),
37
            "vacio": (20, 26)
38
        },
39
        "agua": {
40
            "lleno": (0, 6),
41
            "medio": (7, 12),
42
            "poco": (13, 19),
43
            "vacio": (20, 26)
44
        }
45
    }
46
}
47

    
48
# ================= ESTADO =================
49
estado = {
50
    "comida": {
51
        "distancia_cm": -1,
52
        "nivel": "desconocido",
53
        "porcentaje": 0
54
    },
55
    "agua": {
56
        "distancia_cm": -1,
57
        "nivel": "desconocido",
58
        "porcentaje": 0
59
    }
60
}
61

    
62
# Para controlar la cámara
63
camera = None
64
camera_active = False
65
CONFIG_FILE = "config.json"
66

    
67
# ================= CÁMARA SIMPLIFICADA =================
68
def init_camera():
69
    """Inicializa la cámara"""
70
    global camera, camera_active
71
    
72
    if camera_active and camera is not None:
73
        return True
74
    
75
    # Liberar cámara anterior si existe
76
    if camera is not None:
77
        camera.release()
78
    
79
    # Intentar conectar a la cámara
80
    camera = cv2.VideoCapture(0)
81
    
82
    if not camera.isOpened():
83
        # Intentar con índice alternativo
84
        camera = cv2.VideoCapture(1)
85
        if not camera.isOpened():
86
            print(" No se pudo conectar a la cámara")
87
            camera = None
88
            camera_active = False
89
            return False
90
    
91
    # Configurar parámetros básicos
92
    camera.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
93
    camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
94
    camera.set(cv2.CAP_PROP_FPS, 15)
95
    
96
    camera_active = True
97
    print(" Cámara inicializada correctamente")
98
    return True
99

    
100
def release_camera():
101
    """Libera la cámara"""
102
    global camera, camera_active
103
    
104
    if camera is not None:
105
        camera.release()
106
        camera = None
107
    
108
    camera_active = False
109
    print(" Cámara liberada")
110

    
111
# ================= UTILIDADES =================
112
def cargar_config():
113
    try:
114
        with open(CONFIG_FILE, "r") as f:
115
            return json.load(f)
116
    except:
117
        return {"horarios": []}
118

    
119
def guardar_config(data):
120
    with open(CONFIG_FILE, "w") as f:
121
        json.dump(data, f, indent=2)
122

    
123
def calcular_nivel(distancia_cm, tipo):
124
    if distancia_cm == -1:
125
        return "error", 0
126
    
127
    config = CONFIG_DISPENSADOR["niveles"][tipo]
128
    
129
    if config["lleno"][0] <= distancia_cm <= config["lleno"][1]:
130
        porcentaje = 100 - ((distancia_cm / config["lleno"][1]) * 30)
131
        return "lleno", max(80, min(100, porcentaje))
132
    
133
    elif config["medio"][0] <= distancia_cm <= config["medio"][1]:
134
        porcentaje = 70 - (((distancia_cm - 7) / 5) * 20)
135
        return "medio", max(50, min(79, porcentaje))
136
    
137
    elif config["poco"][0] <= distancia_cm <= config["poco"][1]:
138
        porcentaje = 50 - (((distancia_cm - 13) / 6) * 30)
139
        return "poco", max(20, min(49, porcentaje))
140
    
141
    else:
142
        return "vacio", max(0, min(19, 100 - (distancia_cm * 3)))
143

    
144
def leer_sensores():
145
    global estado
146
    while True:
147
        try:
148
            distancia = grovepi.ultrasonicRead(ULTRA_COMIDA)
149
            nivel, porcentaje = calcular_nivel(distancia, "comida")
150
            estado["comida"] = {
151
                "distancia_cm": distancia,
152
                "nivel": nivel,
153
                "porcentaje": round(porcentaje, 1)
154
            }
155
        except Exception as e:
156
            print(f"Error sensor comida: {e}")
157
            estado["comida"]["nivel"] = "error"
158

    
159
        try:
160
            distancia = grovepi.ultrasonicRead(ULTRA_AGUA)
161
            nivel, porcentaje = calcular_nivel(distancia, "agua")
162
            estado["agua"] = {
163
                "distancia_cm": distancia,
164
                "nivel": nivel,
165
                "porcentaje": round(porcentaje, 1)
166
            }
167
        except Exception as e:
168
            print(f"Error sensor agua: {e}")
169
            estado["agua"]["nivel"] = "error"
170

    
171
        time.sleep(2)
172

    
173
def activar_relay(tipo, segundos):
174
    if tipo == "agua":
175
        grovepi.digitalWrite(RELAY_AGUA, 1)
176
        time.sleep(segundos)
177
        grovepi.digitalWrite(RELAY_AGUA, 0)
178

    
179
def scheduler():
180
    while True:
181
        ahora = datetime.datetime.now()
182
        hora = ahora.strftime("%H:%M")
183
        dia = ahora.strftime("%A")
184

    
185
        config = cargar_config()
186

    
187
        for h in config["horarios"]:
188
            if not h["activo"]:
189
                continue
190
            if h["hora"] != hora:
191
                continue
192
            if dia not in h["dias"]:
193
                continue
194

    
195
            activar_relay(h["tipo"], h["tiempo"])
196

    
197
        time.sleep(60)
198

    
199
# ================= ENDPOINTS BÁSICOS =================
200
@app.route("/")
201
def index():
202
    return jsonify({
203
        "app": "Dispensador IoT",
204
        "status": "running",
205
        "endpoints": {
206
            "/estado": "GET - Estado sensores",
207
            "/niveles": "GET - Niveles procesados",
208
            "/dispensar": "POST - Dispensar manual",
209
            "/horarios": "GET/POST - Horarios",
210
            "/video": "GET - Stream de video MJPEG",
211
            "/foto": "GET - Foto en tiempo real (memoria)",
212
            "/foto-guardar": "POST - Tomar y guardar foto en disco"
213
        }
214
    })
215

    
216
@app.route("/estado", methods=["GET"])
217
def get_estado():
218
    return jsonify({
219
        "comida": estado["comida"],
220
        "agua": estado["agua"],
221
        "timestamp": datetime.datetime.now().isoformat()
222
    })
223

    
224
@app.route("/niveles", methods=["GET"])
225
def get_niveles():
226
    return jsonify({
227
        "comida": {
228
            "nivel": estado["comida"]["nivel"],
229
            "porcentaje": estado["comida"]["porcentaje"]
230
        },
231
        "agua": {
232
            "nivel": estado["agua"]["nivel"],
233
            "porcentaje": estado["agua"]["porcentaje"]
234
        }
235
    })
236

    
237
@app.route("/dispensar", methods=["POST"])
238
def dispensar_manual():
239
    data = request.json
240
    activar_relay(data["tipo"], data["tiempo"])
241
    return jsonify({"ok": True})
242

    
243
@app.route("/horarios", methods=["GET"])
244
def get_horarios():
245
    return jsonify(cargar_config()["horarios"])
246

    
247
@app.route("/horarios", methods=["POST"])
248
def add_horario():
249
    data = request.json
250
    config = cargar_config()
251

    
252
    config["horarios"].append({
253
        "hora": data["hora"],
254
        "dias": data["dias"],
255
        "tipo": data["tipo"],
256
        "tiempo": data["tiempo"],
257
        "activo": True
258
    })
259

    
260
    guardar_config(config)
261
    return jsonify({"ok": True})
262

    
263
# ================= ENDPOINTS CÁMARA =================
264
@app.route("/video")
265
def video():
266
    """Stream MJPEG para WebView"""
267
    # Inicializar cámara si no está activa
268
    if not camera_active:
269
        if not init_camera():
270
            return "No se pudo inicializar la cámara", 500
271
    
272
    def generate():
273
        while camera_active:
274
            success, frame = camera.read()
275
            if not success:
276
                break
277
            
278
            # Convertir frame a JPEG
279
            ret, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
280
            
281
            if ret:
282
                frame_bytes = buffer.tobytes()
283
                yield (b'--frame\r\n'
284
                      b'Content-Type: image/jpeg\r\n\r\n' + 
285
                      frame_bytes + b'\r\n')
286
            
287
            # Controlar FPS (~15 FPS)
288
            time.sleep(0.066)
289
    
290
    return Response(
291
        generate(),
292
        mimetype='multipart/x-mixed-replace; boundary=frame'
293
    )
294

    
295
@app.route("/foto")
296
def foto():
297
    """Foto en tiempo real (solo en memoria) - NUEVA VERSIÓN"""
298
    try:
299
        # Inicializar cámara si no está activa
300
        if not camera_active:
301
            if not init_camera():
302
                return jsonify({"error": "No se pudo inicializar la cámara"}), 500
303
        
304
        success, frame = camera.read()
305
        if not success:
306
            return jsonify({"error": "No se pudo capturar frame"}), 500
307
        
308
        # Convertir frame a JPG en memoria (NO se guarda en disco)
309
        ret, buffer = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 90])
310
        
311
        if not ret:
312
            return jsonify({"error": "Error procesando imagen"}), 500
313
        
314
        image_bytes = buffer.tobytes()
315
        
316
        return Response(
317
            image_bytes,
318
            mimetype="image/jpeg",
319
            headers={
320
                "Content-Disposition": "inline; filename=foto.jpg",
321
                "Cache-Control": "no-cache, no-store, must-revalidate",
322
                "Pragma": "no-cache",
323
                "Expires": "0"
324
            }
325
        )
326
        
327
    except Exception as e:
328
        print(f"❌ Error en endpoint /foto: {e}")
329
        return jsonify({"error": str(e)}), 500
330

    
331
@app.route("/foto-guardar", methods=["POST"])
332
def foto_guardar():
333
    """Tomar y guardar foto en disco (opcional)"""
334
    try:
335
        if not camera_active:
336
            if not init_camera():
337
                return jsonify({"error": "No se pudo inicializar la cámara"}), 500
338
        
339
        success, frame = camera.read()
340
        if not success:
341
            return jsonify({"error": "No se pudo capturar frame"}), 500
342
        
343
        # Guardar en disco
344
        carpeta_destino = "mascotas"
345
        if not os.path.exists(carpeta_destino):
346
            os.makedirs(carpeta_destino)
347

    
348
        ahora = datetime.datetime.now()
349
        nombre_archivo = ahora.strftime("MASCOTA_%Y-%m-%d_%H-%M-%S.jpg")
350
        ruta_completa = os.path.join(carpeta_destino, nombre_archivo)
351

    
352
        exito = cv2.imwrite(ruta_completa, frame)
353
        
354
        if exito:
355
            return jsonify({
356
                "success": True,
357
                "message": "Foto guardada en el servidor",
358
                "filename": nombre_archivo,
359
                "path": ruta_completa,
360
                "timestamp": ahora.isoformat()
361
            })
362
        else:
363
            return jsonify({"error": "No se pudo guardar la foto"}), 500
364
        
365
    except Exception as e:
366
        print(f"❌ Error en endpoint /foto-guardar: {e}")
367
        return jsonify({"error": str(e)}), 500
368

    
369
# ================= MAIN =================
370
if __name__ == "__main__":
371
    # Iniciar sensores en hilos
372
    threading.Thread(target=leer_sensores, daemon=True).start()
373
    threading.Thread(target=scheduler, daemon=True).start()
374
    
375
    print("=" * 60)
376
    print("           DISPENSADOR IoT - FOTO EN MEMORIA")
377
    print("=" * 60)
378
    print("\n SENSORES INICIADOS:")
379
    print(f"  Comida: D{ULTRA_COMIDA}")
380
    print(f"  Agua: D{ULTRA_AGUA}")
381
    print(f"  Relay agua: D{RELAY_AGUA}")
382
    
383
    print("\n ENDPOINTS CÁMARA:")
384
    print("  GET   /video           - Stream MJPEG")
385
    print("  GET   /foto            - Foto en tiempo real (memoria)")
386
    print("  POST  /foto-guardar    - Tomar y guardar foto en disco")
387
    
388
    print("=" * 60)
389
    print(f"\n Servidor iniciado en: http://0.0.0.0:5000")
390
    print(" Video en vivo: http://<TU_IP>:5000/video")
391
    print(" Foto instantánea: http://<TU_IP>:5000/foto")
392
    print("=" * 60)
393
    
394
    try:
395
        app.run(host="0.0.0.0", port=5000, threaded=True, debug=False)
396
    except KeyboardInterrupt:
397
        print("\n Cerrando servidor...")
398
        release_camera()
399
    finally:
400
        release_camera()