Router WiFi con Raspberry Pi usando el sistema oficial

La siguiente aplicación permite controlar el modo punto de acceso (AP) y cliente (STA) de tu Raspberry Pi mediante una interfaz web.

wifi_control.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Controlador WiFi unificado con interfaz web (Flask + plantillas embebidas)
— Área superior de configuración para eth0/puertos/estrategia de enlace —
"""

# ======== Zona de configuración (personalizable) ========
RED_FISICA = "eth0"           # Interfaz Ethernet vinculada y mostrada
MODO_ENLACE = "auto"          # "auto"=usar IPv4 de RED_FISICA; "manual"=usar IP_ENLACE
IP_ENLACE = "0.0.0.0"         # Valor efectivo cuando MODO_ENLACE="manual"
PUERTO_WEB = 5000             # Puerto de escucha para Flask
# ==========================================================

import os
import subprocess
from flask import Flask, render_template_string, request, redirect, url_for, flash

aplicacion = Flask(__name__)
aplicacion.secret_key = 'clave-secreta-wifi'  # Para mensajes flash

ssid_ap_memoria = ""
clave_ap_memoria = ""

ssid_sta_memoria = ""
clave_sta_memoria = ""

PLANTILLA = r"""



  <meta charset="UTF-8"></meta>
  <title>Control WiFi</title>
  <style>
    body { font-family: Arial, sans-serif; background:#f0f2f5; margin:0; padding:20px; }
    .seccion { max-width: 900px; margin: 20px auto; padding: 20px; border-radius: 15px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); }
    .ap-seccion { background: linear-gradient(to right, #2196F3, #21CBF3); color: #fff; }
    .sta-seccion { background: linear-gradient(to right, #9C27B0, #E040FB); color: #fff; }
    h2 { margin-top: 0; }
    input[type="text"], input[type="password"] { padding: 10px; margin: 5px 0; width: 100%; border-radius: 8px; border: none; }
    .cuadricula-botones { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 15px; }
    button { padding: 12px; border: none; border-radius: 25px; background: linear-gradient(to right, #4CAF50, #81C784); color: white; font-weight: bold; cursor: pointer; transition: background 0.3s ease; width: 100%; }
    button:hover { background: linear-gradient(to right, #388E3C, #66BB6A); }
    .lista-clientes, .lista-wifi { margin-top: 10px; padding: 10px; background: white; color: black; border-radius: 10px; }
    .senal-wifi { float: right; }
    .flash { background: #fff3cd; color: #856404; border: 1px solid #ffeeba; padding: 10px; margin-bottom: 10px; border-radius: 5px; }
    label { display: block; margin-top: 8px; }
    .checkbox { display: flex; align-items: center; gap: 10px; margin: 10px 0; }
    .barra-superior { max-width:900px; margin:0 auto; padding:8px 12px; background:#fff; border-radius: 10px; box-shadow: 0 1px 3px rgba(0,0,0,.1); color:#333; }
    .barra-superior code { background:#f7f7f7; padding:2px 6px; border-radius:6px; }
  </style>
  <script>
    function toggleCampoClave() {
      const apAbierto = document.getElementById('ap_abierto').checked;
      const campoClave = document.getElementById('campo_clave_ap');
      if (campoClave) campoClave.style.display = apAbierto ? 'none' : 'block';
    }
    function rellenarSSID(ssid) {
      document.getElementById('entrada_ssid_sta').value = ssid;
      window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
    }
  </script>



  <div class="barra-superior">
    <div><strong>Interfaz vinculada:</strong> {{ interfaz }} |
         <strong>IP interfaz:</strong> {{ ip_interfaz || 'No disponible' }} |
         <strong>Dirección servicio:</strong> {{ host_enlace }}:{{ puerto }}</div>
  </div>

  {% with mensajes = get_flashed_messages(with_categories=true) %}
    {% if mensajes %}
      {% for categoria, mensaje in mensajes %}
        <div class="flash">{{ mensaje }}</div>
      {% endfor %}
    {% endif %}
  {% endwith %}

  
  <div class="seccion ap-seccion">
    <h2>Modo Punto de Acceso (AP)</h2>

    <form action="/wifi/activar_ap" method="POST">
      <label>SSID:</label>
      <input name="ssid_ap" required="" type="text" value="{{ ultimo_ssid }}"></input>

      <div class="checkbox">
        <input id="ap_abierto" name="ap_abierto" onchange="toggleCampoClave()" type="checkbox"></input>
        <label for="ap_abierto">AP Abierto (Sin Contraseña)</label>
      </div>

      <div id="campo_clave_ap">
        <label>Contraseña:</label>
        <input name="clave_ap" type="password" value="{{ ultima_clave }}"></input>
      </div>

      <div class="cuadricula-botones">
        <button type="submit">Iniciar AP</button>
      </div>
    </form>

    <div class="cuadricula-botones" style="margin-top:10px;">
      <form action="/wifi/desactivar_ap" method="POST"><button type="submit">Detener AP</button></form>
      <form action="/wifi/reiniciar_wifi" method="POST"><button type="submit">Reiniciar WiFi</button></form>
      <form action="/wifi/limpiar-conexiones" method="POST"><button type="submit">Limpiar Config AP</button></form>
    </div>

    {% if estado_ap %}
      <p><strong>Estado:</strong> {{ estado_ap }}</p>
      <p><strong>Dispositivos Conectados:</strong> {{ conteo_clientes }}</p>
      <div class="lista-clientes">
        {% for mac in macs_clientes %}
          <div>{{ mac }}</div>
        {% endfor %}
      </div>
    {% endif %}
  </div>

  
  <div class="seccion sta-seccion">
    <h2>Modo Cliente (STA)</h2>

    <form action="/wifi/activar_sta" method="POST">
      <label>SSID:</label>
      <input id="entrada_ssid_sta" name="ssid_sta" required="" type="text" value="{{ ssid_sta }}"></input>
      <label>Contraseña:</label>
      <input name="clave_sta" required="" type="text" value="{{ clave_sta }}"></input>

      <div class="cuadricula-botones">
        <button type="submit">Conectar STA</button>
      </div>
    </form>

    <div class="cuadricula-botones" style="margin-top:10px;">
      <form action="/wifi/desactivar_sta" method="POST"><button type="submit">Desconectar STA</button></form>
      <form action="/wifi/escanear_wifi" method="POST"><button type="submit">Escanear WiFi</button></form>
    </div>

    <div class="lista-wifi">
      {% for red in redes %}
        <form action="/wifi/seleccionar_sta" method="POST">
          <input name="ssid_sta" type="hidden" value="{{ red.ssid }}"></input>
          <button style="width: 100%;" title="Hacer clic para seleccionar y rellenar el SSID superior, luego ingrese la contraseña para conectarse" type="submit">
            {{ red.ssid }} <span class="senal-wifi">{{ senal }}%</span>
          </button>
        </form>
      {% endfor %}
    </div>
  </div>



"""

def ejecutar_comando(comando):
    try:
        salida = subprocess.check_output(comando, shell=True, stderr=subprocess.STDOUT, text=True)
        filtrada = "\n".join(
            linea for linea in salida.splitlines()
            if "not active" not in linea and "not all devices disconnected" not in linea
        )
        return filtrada.strip()
    except subprocess.CalledProcessError as e:
        filtrada = "\n".join(
            linea for linea in e.output.splitlines()
            if "not active" not in linea and "not all devices disconnected" not in linea
        )
        return filtrada.strip()

def obtener_ip_interfaz(interfaz: str) -> str:
    """
    Obtiene la dirección IPv4 de una interfaz de red específica (priorizando inet).
    Sin dependencias externas, utiliza el comando `ip` para análisis.
    """
    comando = f"ip -4 addr show dev {interfaz} | grep -oP '(?<=inet\\s)\\d+\\.\\d+\\.\\d+\\.\\d+' | head -n1"
    ip = ejecutar_comando(comando)
    return ip if ip else ""

def obtener_estado():
    estado_ap = ejecutar_comando("nmcli -t -f NAME,TYPE con show --active | grep 'wifi' | grep 'ap'").strip()
    return estado_ap

def obtener_clientes_conectados():
    resultado = ejecutar_comando("iw dev wlan0 station dump | grep Station").strip()
    clientes = resultado.splitlines() if resultado else []
    return len(clientes), clientes

def reiniciar_wifi():
    ejecutar_comando("nmcli radio wifi off")
    ejecutar_comando("nmcli radio wifi on")
    ejecutar_comando("nmcli connection reload")
    ejecutar_comando("nmcli device set wlan0 managed yes")
    ejecutar_comando("nmcli dev disconnect wlan0")

def escanear_wifi():
    resultado = ejecutar_comando("nmcli -t -f SSID,SIGNAL dev wifi list")
    redes = []
    for linea in resultado.splitlines():
        partes = linea.split(":")
        if len(partes) >= 2 and partes[0]:
            redes.append({"ssid": partes[0], "senal": partes[1]})
    # Eliminar duplicados: a veces el escaneo repite SSID
    unicos = {}
    for r in redes:
        if r["ssid"] not in unicos or int(r["senal"] or 0) > int(unicos[r["ssid"]]["senal"] or 0):
            unicos[r["ssid"]] = r
    return list(unicos.values())

@aplicacion.route('/')
def inicio():
    estado_ap = obtener_estado()
    conteo_clientes, macs_clientes = obtener_clientes_conectados()
    redes = escanear_wifi()
    ip_interfaz = obtener_ip_interfaz(RED_FISICA)
    host_enlace = ip_interfaz if MODO_ENLACE == "auto" and ip_interfaz else (IP_ENLACE if MODO_ENLACE == "manual" else "0.0.0.0")
    flash("WiFi inicializado al arrancar", "info")
    return render_template_string(
        PLANTILLA,
        estado_ap=estado_ap,
        ultimo_ssid=ssid_ap_memoria,
        ultima_clave=clave_ap_memoria,
        ssid_sta=ssid_sta_memoria,
        clave_sta=clave_sta_memoria,
        redes=redes,
        conteo_clientes=conteo_clientes,
        macs_clientes=macs_clientes,
        interfaz=RED_FISICA,
        ip_interfaz=ip_interfaz,
        host_enlace=host_enlace,
        puerto=PUERTO_WEB
    )

@aplicacion.route('/wifi/activar_ap', methods=['POST'])
def activar_ap():
    global ssid_ap_memoria, clave_ap_memoria
    ssid = request.form.get('ssid_ap', '').strip()
    clave = request.form.get('clave_ap', '')
    ap_abierto = request.form.get('ap_abierto')
    nombre_con = ssid

    if not ssid:
        flash("Falta SSID del AP", "error")
        return redirect(url_for('inicio'))

    ssid_ap_memoria = ssid
    clave_ap_memoria = clave if clave else ""

    reiniciar_wifi()

    antiguas_conexiones = ejecutar_comando(f"nmcli -t -f UUID,NAME,DEVICE connection show | grep '{ssid}'")
    for linea in filter(None, antiguas_conexiones.strip().split('\n')):
        if ssid in linea:
            uuid = linea.split(':')[0]
            ejecutar_comando(f"nmcli connection delete uuid {uuid}")

    ejecutar_comando(f"nmcli connection add type wifi ifname wlan0 mode ap con-name '{nombre_con}' ssid '{ssid}'")

    if ap_abierto:
        ejecutar_comando(f"nmcli connection modify '{nombre_con}' 802-11-wireless.band bg ipv4.method shared ifname wlan0")
    else:
        ejecutar_comando(f"nmcli connection modify '{nombre_con}' 802-11-wireless.band bg ipv4.method shared ifname wlan0 wifi-sec.key-mgmt wpa-psk wifi-sec.psk '{clave}'")

    ejecutar_comando("nmcli connection reload")
    resultado_activacion = ejecutar_comando(f"nmcli connection up '{nombre_con}' ifname wlan0")

    if "Error" in resultado_activacion:
        reiniciar_wifi()
        resultado_activacion = ejecutar_comando(f"nmcli connection up '{nombre_con}' ifname wlan0")
        if "Error" in resultado_activacion:
            flash("No se pudo iniciar AP. Intente reiniciar WiFi.", "error")
        else:
            flash(f"Resultado de inicio AP: {resultado_activacion}", "info")
    else:
        flash(f"Resultado de inicio AP: {resultado_activacion}", "info")

    return redirect(url_for('inicio'))

@aplicacion.route('/wifi/desactivar_ap', methods=['POST'])
def desactivar_ap():
    conexiones_activas = ejecutar_comando("nmcli -t -f NAME con show --active").splitlines()
    if ssid_ap_memoria and ssid_ap_memoria in conexiones_activas:
        resultado = ejecutar_comando(f"nmcli connection down '{ssid_ap_memoria}'")
        flash(f"Resultado de detención AP: {resultado}", "info")
    else:
        flash("AP no estaba activo", "warning")
    return redirect(url_for('inicio'))

@aplicacion.route('/wifi/reiniciar_wifi', methods=['POST'])
def reiniciar_wifi_ruta():
    reiniciar_wifi()
    flash("WiFi ha sido reiniciado", "info")
    return redirect(url_for('inicio'))

@aplicacion.route('/wifi/limpiar-conexiones', methods=['POST'])
def limpiar_conexiones():
    residuos = ejecutar_comando("nmcli -t -f UUID,NAME connection show")
    for linea in filter(None, residuos.strip().split('\n')):
        if ssid_ap_memoria and ssid_ap_memoria in linea:
            uuid = linea.split(':')[0]
            ejecutar_comando(f"nmcli connection delete uuid {uuid}")
    flash("Conexiones antiguas de hotspot eliminadas", "info")
    return redirect(url_for('inicio'))

@aplicacion.route('/wifi/activar_sta', methods=['POST'])
def activar_sta():
    global ssid_sta_memoria, clave_sta_memoria
    ssid = request.form.get('ssid_sta', '').strip()
    clave = request.form.get('clave_sta', '')
    if not ssid:
        flash("Falta SSID del STA", "error")
        return redirect(url_for('inicio'))
    ssid_sta_memoria = ssid
    clave_sta_memoria = clave
    resultado = ejecutar_comando(f"nmcli dev wifi connect '{ssid}' password '{clave}'")
    flash(f"Resultado de conexión STA: {resultado}", "info")
    return redirect(url_for('inicio'))

@aplicacion.route('/wifi/desactivar_sta', methods=['POST'])
def desactivar_sta():
    resultado = ejecutar_comando("nmcli dev disconnect wlan0")
    flash(f"Resultado de desconexión STA: {resultado}", "info")
    return redirect(url_for('inicio'))

@aplicacion.route('/wifi/escanear_wifi', methods=['POST'])
def escanear_wifi_ruta():
    # El escaneo se obtiene en tiempo real en inicio()
    flash("Escaneo WiFi actualizado.", "info")
    return redirect(url_for('inicio'))

@aplicacion.route('/wifi/seleccionar_sta', methods=['POST'])
def seleccionar_sta():
    global ssid_sta_memoria
    ssid_sta_memoria = request.form.get('ssid_sta', '')
    flash(f"SSID seleccionado '{ssid_sta_memoria}', ingrese contraseña para conectarse.", "info")
    return redirect(url_for('inicio'))

def _limpiar_conexiones_ap_al_inicio():
    if not ssid_ap_memoria:
        return
    residuos = ejecutar_comando("nmcli -t -f UUID,NAME connection show")
    for linea in filter(None, residuos.strip().split('\n')):
        if ssid_ap_memoria in linea:
            uuid = linea.split(':')[0]
            ejecutar_comando(f"nmcli connection delete uuid {uuid}")

if __name__ == '__main__':
    _limpiar_conexiones_ap_al_inicio()
    reiniciar_wifi()

    ip_interfaz = obtener_ip_interfaz(RED_FISICA)
    host = ip_interfaz if (MODO_ENLACE == "auto" and ip_interfaz) else (IP_ENLACE if MODO_ENLACE == "manual" else "0.0.0.0")
    print(f"[INFO] Usando RED_FISICA={RED_FISICA}, IP detectada={ip_interfaz or 'N/A'}, host de enlace={host}, puerto={PUERTO_WEB}")
    aplicacion.run(host=host, port=PUERTO_WEB)

Ejecución en Terminal

sudo apt install python3 python3-pip -y
sudo apt install python3-flask python3-serial
# O # sudo pip3 install flask
cd control_wifi
# Iniciar
sudo python3 wifi_control.py
# Detener
sudo pkill -f wifi_control.py

sudo systemctl restart NetworkManager
nmcli radio wifi off
nmcli radio wifi on

Configuración de un Punto de Acceso

Para crear un punto de acceso con SSID "miwifi" y contraseña "12345678", puedes usar los siguientes comandos:

Crear y guardar una conexión de hotspot:

sudo nmcli connection add type wifi ifname wlan0 con-name miwifi autoconnect yes ssid miwifi

Configurar en modo AP y compartir red:

sudo nmcli connection modify miwifi 802-11-wireless.mode ap 802-11-wireless.band bg ipv4.method shared

Establecer seguridad como red abierta:

sudo nmcli connection modify miwifi wifi-sec.key-mgmt none

Finalmente activar:

sudo nmcli connection up miwifi

Esta configuración se guardará en el sistema, y después de reiniciar la Raspberry Pi, el hotspot estará disponible automáticamente sin necesidad de ejecutar comandos manualmente.

Prueba de Velocidad Web para Raspberry Pi

El siguiente script permite realizar pruebas de velocidad de red mediante una interfaz web:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import time
import socket
import fcntl
import struct
import threading
from flask import Flask, jsonify, render_template_string, request
from waitress import serve
import requests

PUERTO_HTTP = 12345
INTERFACES = ["eth0", "wlan0"]

# Puede reemplazar las fuentes de prueba, prefiera HTTP para evitar problemas de certificado
URLS_PRUEBA = [
    "http://speedtest.tele2.net/100MB.zip",
    "http://ipv4.download.thinkbroadband.com/100MB.zip",
]

app = Flask(__name__)

# Estado global de prueba de velocidad
estado_velocidad = {
    "ejecutando": False,
    "finalizado": False,
    "error": "",
    "url": "",
    "tiempo_inicio": 0,
    "transcurrido": 0.0,
    "bajados_bytes": 0,
    "instantaneo_mbps": 0.0,
    "promedio_mbps": 0.0,
    "registro": [],
}
bloqueo_estado = threading.Lock()

def obtener_ip_interfaz(nombre_interfaz: str) -> str:
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        return socket.inet_ntoa(
            fcntl.ioctl(
                s.fileno(),
                0x8915,
                struct.pack("256s", nombre_interfaz[:15].encode("utf-8"))
            )[20:24]
        )
    except Exception:
        return ""

def obtener_todas_ips():
    elementos = []
    for iface in INTERFACES:
        ip = obtener_ip_interfaz(iface)
        if ip:
            elementos.append(f"{iface}: {ip}:{PUERTO_HTTP}")
    if not elementos:
        try:
            host = socket.gethostname()
            ip = socket.gethostbyname(host)
            elementos.append(f"host: {ip}:{PUERTO_HTTP}")
        except Exception:
            elementos.append(f"127.0.0.1:{PUERTO_HTTP}")
    return elementos

def reiniciar_estado():
    with bloqueo_estado:
        estado_velocidad["ejecutando"] = False
        estado_velocidad["finalizado"] = False
        estado_velocidad["error"] = ""
        estado_velocidad["url"] = ""
        estado_velocidad["tiempo_inicio"] = 0
        estado_velocidad["transcurrido"] = 0.0
        estado_velocidad["bajados_bytes"] = 0
        estado_velocidad["instantaneo_mbps"] = 0.0
        estado_velocidad["promedio_mbps"] = 0.0
        estado_velocidad["registro"] = []

def agregar_registro(msg: str):
    with bloqueo_estado:
        estado_velocidad["registro"].append(msg)
        if len(estado_velocidad["registro"]) > 200:
            estado_velocidad["registro"] = estado_velocidad["registro"][-200:]

def seleccionar_url_prueba(url_personalizada: str = "") -> str:
    if url_personalizada.strip():
        return url_personalizada.strip()
    for url in URLS_PRUEBA:
        return url
    return ""

def trabajador_prueba_velocidad(url: str, max_segundos: int = 20):
    reiniciar_estado()
    with bloqueo_estado:
        estado_velocidad["ejecutando"] = True
        estado_velocidad["url"] = url
        estado_velocidad["tiempo_inicio"] = time.time()

    agregar_registro(f"Iniciando prueba: {url}")

    bajados = 0
    ultimos_bytes = 0
    ultimo_tiempo = time.time()
    tiempo_inicio = ultimo_tiempo

    try:
        cabeceras = {
            "User-Agent": "RaspberryPi-PruebaVelocidad/1.0"
        }

        with requests.get(url, stream=True, timeout=(5, 10), headers=cabeceras) as resp:
            resp.raise_for_status()

            tamano_bloque = 64 * 1024  # 64KB
            for bloque in resp.iter_content(chunk_size=tamano_bloque):
                if not bloque:
                    continue

                ahora = time.time()
                bajados += len(bloque)
                transcurrido = ahora - tiempo_inicio

                # Actualizar velocidad instantánea cada 0.5 segundos
                if ahora - ultimo_tiempo >= 0.5:
                    delta_bytes = bajados - ultimos_bytes
                    delta_tiempo = ahora - ultimo_tiempo
                    instantaneo_mbps = (delta_bytes * 8 / delta_tiempo) / 1_000_000
                    promedio_mbps = (bajados * 8 / transcurrido) / 1_000_000 if transcurrido > 0 else 0.0

                    with bloqueo_estado:
                        estado_velocidad["transcurrido"] = round(transcurrido, 2)
                        estado_velocidad["bajados_bytes"] = bajados
                        estado_velocidad["instantaneo_mbps"] = round(instantaneo_mbps, 2)
                        estado_velocidad["promedio_mbps"] = round(promedio_mbps, 2)

                    agregar_registro(
                        f"Descargados {bajados / 1024 / 1024:.2f} MB, "
                        f"Instantáneo {instantaneo_mbps:.2f} Mbps, Promedio {promedio_mbps:.2f} Mbps"
                    )

                    ultimo_tiempo = ahora
                    ultimos_bytes = bajados

                # Alcanzar el tiempo máximo de prueba, detenerse
                if transcurrido >= max_segundos:
                    agregar_registro(f"Alcanzado tiempo máximo {max_segundos} segundos, deteniendo prueba")
                    break

        total_transcurrido = time.time() - tiempo_inicio
        promedio_final_mbps = (bajados * 8 / total_transcurrido) / 1_000_000 if total_transcurrido > 0 else 0.0

        with bloqueo_estado:
            estado_velocidad["transcurrido"] = round(total_transcurrido, 2)
            estado_velocidad["bajados_bytes"] = bajados
            estado_velocidad["promedio_mbps"] = round(promedio_final_mbps, 2)
            estado_velocidad["instantaneo_mbps"] = 0.0
            estado_velocidad["finalizado"] = True
            estado_velocidad["ejecutando"] = False

        agregar_registro(
            f"Prueba completada: Total descargado {bajados / 1024 / 1024:.2f} MB, "
            f"Tiempo total {total_transcurrido:.2f} s, Velocidad promedio {promedio_final_mbps:.2f} Mbps"
        )

    except Exception as e:
        with bloqueo_estado:
            estado_velocidad["error"] = str(e)
            estado_velocidad["ejecutando"] = False
            estado_velocidad["finalizado"] = True
        agregar_registro(f"Prueba fallida: {e}")

HTML_PRUEBA = """



    <meta charset="utf-8"></meta>
    <title>Prueba de Velocidad Web para Raspberry Pi</title>
    <meta content="width=device-width, initial-scale=1" name="viewport"></meta>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f5f6f7;
            color: #222;
        }
        .contenedor {
            max-width: 900px;
            margin: 0 auto;
        }
        .tarjeta {
            background: #fff;
            border-radius: 12px;
            padding: 18px;
            margin-bottom: 16px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
        }
        h1 {
            margin: 0 0 10px 0;
            font-size: 26px;
        }
        .pequeno {
            color: #666;
            font-size: 14px;
        }
        input[type=text], input[type=number] {
            width: 100%;
            box-sizing: border-box;
            padding: 10px;
            border: 1px solid #ccc;
            border-radius: 8px;
            margin-top: 6px;
            margin-bottom: 10px;
            font-size: 14px;
        }
        button {
            padding: 10px 16px;
            border: none;
            border-radius: 8px;
            background: #1677ff;
            color: white;
            cursor: pointer;
            font-size: 14px;
            margin-right: 8px;
        }
        button:disabled {
            background: #999;
            cursor: not-allowed;
        }
        .cuadricula {
            display: grid;
            grid-template-columns: repeat(2, 1fr);
            gap: 12px;
        }
        .metrica {
            background: #fafafa;
            border-radius: 10px;
            padding: 12px;
            border: 1px solid #eee;
        }
        .metrica .etiqueta {
            color: #666;
            font-size: 13px;
        }
        .metrica .valor {
            font-size: 24px;
            margin-top: 6px;
            font-weight: bold;
        }
        pre {
            background: #111;
            color: #0f0;
            padding: 12px;
            border-radius: 10px;
            overflow: auto;
            min-height: 220px;
            white-space: pre-wrap;
            word-break: break-word;
        }
        .estado {
            font-weight: bold;
            margin-top: 6px;
        }
    </style>


<div class="contenedor">
    <div class="tarjeta">
        <h1>Prueba de Velocidad Web para Raspberry Pi</h1>
        <div class="pequeno">Direcciones de acceso:</div>
        <div class="pequeno">
            {% for ip in ips %}
                <div>{{ ip }}</div>
            {% endfor %}
        </div>
    </div>

    <div class="tarjeta">
        <label>URL de prueba (dejar vacío para usar predeterminado)</label>
        <input id="url" placeholder="Ejemplo: http://speedtest.tele2.net/100MB.zip" type="text"></input>

        <label>Duración de prueba (segundos)</label>
        <input id="segundos" max="120" min="3" type="number" value="20"></input>

        <button id="btnInicio" onclick="iniciarPrueba()">Iniciar Prueba</button>
        <button onclick="actualizarEstado()">Actualizar Estado</button>
        <div class="estado" id="textoEstado">Estado actual: Inactivo</div>
    </div>

    <div class="tarjeta">
        <div class="cuadricula">
            <div class="metrica">
                <div class="etiqueta">Velocidad instantánea</div>
                <div class="valor" id="instantaneo">0 Mbps</div>
            </div>
            <div class="metrica">
                <div class="etiqueta">Velocidad promedio</div>
                <div class="valor" id="promedio">0 Mbps</div>
            </div>
            <div class="metrica">
                <div class="etiqueta">Datos descargados</div>
                <div class="valor" id="bajados">0 MB</div>
            </div>
            <div class="metrica">
                <div class="etiqueta">Tiempo transcurrido</div>
                <div class="valor" id="transcurrido">0 s</div>
            </div>
        </div>
    </div>

    <div class="tarjeta">
        <div style="margin-bottom:8px;font-weight:bold;">Registro</div>
        
    </div>
</div>

<script>
let temporizador = null;

function bytesAMB(bytes) {
    return (bytes / 1024 / 1024).toFixed(2);
}

function actualizarUI(datos) {
    document.getElementById("instantaneo").innerText = (datos.instantaneo_mbps || 0) + " Mbps";
    document.getElementById("promedio").innerText = (datos.promedio_mbps || 0) + " Mbps";
    document.getElementById("bajados").innerText = bytesAMB(datos.bajados_bytes || 0) + " MB";
    document.getElementById("transcurrido").innerText = (datos.transcurrido || 0) + " s";

    let estado = "Inactivo";
    if (datos.ejecutando) estado = "Ejecutando prueba";
    else if (datos.finalizado && !datos.error) estado = "Completado";
    else if (datos.error) estado = "Fallido: " + datos.error;
    document.getElementById("textoEstado").innerText = "Estado actual: " + estado;

    document.getElementById("cajaRegistro").innerText = (datos.registro || []).join("\\n");

    const btnInicio = document.getElementById("btnInicio");
    btnInicio.disabled = !!datos.ejecutando;

    if (!datos.ejecutando && temporizador) {
        clearInterval(temporizador);
        temporizador = null;
    }
}

function actualizarEstado() {
    fetch("/estado")
        .then(r => r.json())
        .then(datos => actualizarUI(datos))
        .catch(err => {
            document.getElementById("textoEstado").innerText = "Estado actual: Error al obtener estado";
        });
}

function iniciarPrueba() {
    const url = document.getElementById("url").value.trim();
    const segundos = parseInt(document.getElementById("segundos").value || "20", 10);

    fetch("/iniciar", {
        method: "POST",
        headers: {"Content-Type": "application/json"},
        body: JSON.stringify({url: url, segundos: segundos})
    })
    .then(r => r.json())
    .then(datos => {
        actualizarUI(datos);
        if (!temporizador) {
            temporizador = setInterval(actualizarEstado, 1000);
        }
    })
    .catch(err => {
        document.getElementById("textoEstado").innerText = "Estado actual: Error al iniciar prueba";
    });
}

actualizarEstado();
</script>


"""

@aplicacion.route("/")
def pagina_principal():
    return render_template_string(HTML_PRUEBA, ips=obtener_todas_ips())

@aplicacion.route("/estado")
def estado_prueba():
    with bloqueo_estado:
        return jsonify(estado_velocidad)

@aplicacion.route("/iniciar", methods=["POST"])
def iniciar_prueba():
    with bloqueo_estado:
        if estado_velocidad["ejecutando"]:
            return jsonify(estado_velocidad)

    datos = request.get_json(silent=True) or {}
    url_personalizada = str(datos.get("url", "")).strip()
    segundos = int(datos.get("segundos", 20))
    segundos = max(3, min(segundos, 120))

    url = seleccionar_url_prueba(url_personalizada)
    if not url:
        return jsonify({"error": "No se configuró URL de prueba"})

    hilo = threading.Thread(target=trabajador_prueba_velocidad, args=(url, segundos), daemon=True)
    hilo.start()

    time.sleep(0.2)
    with bloqueo_estado:
        return jsonify(estado_velocidad)

if __name__ == "__main__":
    print("Servicio de prueba de velocidad web iniciado")
    for item in obtener_todas_ips():
        print("Dirección de acceso:", f"http://{item}")

    serve(aplicacion, host="0.0.0.0", port=PUERTO_HTTP, threads=8)

Configuración del Modo Cliente (STA)

Para conectar tu Raspberry Pi a una red WiFi existente, puedes utilizar el comando nmcli:

# Ver redes WiFi disponibles
nmcli dev wifi list

# Conectar a una red WiFi
nmcli dev wifi connect "Nombre_de_tu_red" password "tu_contraseña"

# Ver estado de conexión
nmcli dev status

# Ver conexión WiFi activa
nmcli connection show --active

# Desconectar WiFi
nmcli dev disconnect wlan0

# Configurar conexión automática
nmcli connection modify "Nombre_de_tu_red" connection.autoconnect yes

Configuración Alternativa con wpa_supplicant

Si prefieres configurar WiFi mediante el archivo wpa_supplicant.conf:

# Editar archivo de configuración WiFi
sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

# Agregar configuración de red
network={
    ssid="Nombre_de_tu_red"
    psk="tu_contraseña"
    key_mgmt=WPA2-PSK
}

# Guardar archivo (Ctrl+X, Y, Enter)

# Reiniciar servicio de red
sudo systemctl restart dhcpcd

# Verificar conexión
ifconfig wlan0

Etiquetas: raspberry-pi WiFi Flask nmcli punto-de-acceso

Publicado el 6-16 20:58