Conexión a través de Wi-Fi

En esta sección se mostrará como conectar un dispositivo al ESP32 y enviar comandos via un navegador.

Dependencias de AtomVM

Para obtener acceso a la librería httpd de Erlang, es necesario incluir atomvm_lib en el archivo mix.exs.

mix.exs
defp deps do
  [
    {:exatomvm, git: "https://github.com/atomvm/ExAtomVM/"},
    {:atomvm_lib, git: "https://github.com/atomvm/atomvm_lib/"}
  ]
end

Ejemplo

A continuación, se incluye un módulo de ejemplo que implementa httpd para servir archivos y una API desde un servidor hosteado en la ESP32.

defmodule ApTest do
  @behaviour :httpd_api_handler

  @port 8080

  def start() do
    ap_config = [
      ssid: "robot test",
      psk: "passpass",
      ap_started: fn -> :io.format('Punto de acceso iniciado en 192.168.4.1~n') end
    ]

    {:ok, _network_pid} = :network.start(ap: ap_config)

    Process.sleep(1000)

    httpd_config = [
      # handler para ruta /api
      {[<<"api">>],
       %{
         handler: :httpd_api_handler,
         handler_config: %{
           module: __MODULE__
         }
       }},
      # handler de archivos para ruta /, expone archivos en directorio /priv
      {[],
       %{
         handler: :httpd_file_handler,
         handler_config: %{
           app: :serial_test
         }
       }}
    ]

    {:ok, _httpd_pid} = :httpd.start_link(@port, httpd_config)

    :io.format('Servidor web iniciado en http://192.168.4.1:8080~n')

    :timer.sleep(:infinity)
  end

  def handle_api_request(:post, [<<"move">>], http_request, _args) do
    direction = http_request.body

    move_robot(direction)

    {:ok, %{status: "ok"}}
  end

  def handle_api_request(method, path, _http_request, _args) do
    :io.format('Petición no soportada: ~p ~p~n', [method, path])
    :not_found
  end

  defp move_robot(direction) do
    :io.format('Comando de movimiento recibido: ~p~n', [direction])
    # aqui iría el código para mover el robot
  end
end

También es necesario incluir un archivo index.html en el directorio /priv del proyecto (se debe crear si no existe), para acceder a la API a través de una interfaz.

priv/index.html
<!DOCTYPE html>
<html>
<head>
    <title>Control del Robot</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        :root {
            --bg: #0f1220;
            --panel: #1a1e35;
            --btn: #2a2f55;
            --btn-hover: #3a4090;
            --btn-active: #4c53c7;
            --text: #e8eaff;
            --danger: #c0392b;
        }

        body {
            margin: 0;
            min-height: 100vh;
            font-family: system-ui, sans-serif;
            background: radial-gradient(circle at top, #1a1e35, #0f1220);
            color: var(--text);
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
        }

        h1 {
            margin-bottom: 24px;
            font-weight: 600;
        }

        .controls {
            background: var(--panel);
            padding: 20px;
            border-radius: 20px;
            display: grid;
            grid-template-columns: repeat(3, 80px);
            gap: 12px;
            box-shadow: 0 10px 30px rgba(0,0,0,0.4);
        }

        .controls button {
            width: 80px;
            height: 80px;
            border-radius: 18px;
            border: none;
            background: linear-gradient(180deg, var(--btn), #1f2340);
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow:
                0 6px 0 #14172b,
                0 10px 20px rgba(0,0,0,0.4);
            transition: transform 0.05s ease, box-shadow 0.05s ease, background 0.15s;
        }

        .controls button:hover {
            background: linear-gradient(180deg, var(--btn-hover), #2b3090);
        }

        .controls button:active {
            transform: translateY(4px);
            box-shadow:
                0 2px 0 #14172b,
                0 4px 10px rgba(0,0,0,0.4);
            background: linear-gradient(180deg, var(--btn-active), #3a40b0);
        }

        .controls svg {
            width: 36px;
            height: 36px;
            fill: var(--text);
        }

        .forward { grid-column: 2; }
        .left    { grid-column: 1; }
        .stop    {
            grid-column: 2;
            background: linear-gradient(180deg, var(--danger), #8e1f17);
        }
        .right   { grid-column: 3; }
        .back    { grid-column: 2; }

        .stop:hover {
            background: linear-gradient(180deg, #e74c3c, #a93226);
        }

        .status {
            margin-top: 24px;
            font-size: 14px;
            opacity: 0.9;
        }
    </style>
</head>
<body>
    <h1>Control del Robot</h1>

    <div class="controls">
        <button class="forward" onclick="moveRobot('forward')">
            <svg viewBox="0 0 24 24">
                <path d="M12 4l-8 8h5v8h6v-8h5z"/>
            </svg>
        </button>

        <button class="left" onclick="moveRobot('left')">
            <svg viewBox="0 0 24 24">
                <path d="M4 12l8 8v-5h8v-6h-8v-5z"/>
            </svg>
        </button>

        <button class="stop" onclick="moveRobot('stop')">
            <svg viewBox="0 0 24 24">
                <rect x="6" y="6" width="12" height="12"/>
            </svg>
        </button>

        <button class="right" onclick="moveRobot('right')">
            <svg viewBox="0 0 24 24">
                <path d="M20 12l-8-8v5h-8v6h8v5z"/>
            </svg>
        </button>

        <button class="back" onclick="moveRobot('back')">
            <svg viewBox="0 0 24 24">
                <path d="M12 20l8-8h-5v-8h-6v8h-5z"/>
            </svg>
        </button>
    </div>

    <div id="status" class="status">Estado: Listo</div>

    <script>
        function moveRobot(direction) {
            document.getElementById("status").innerText = "Estado: Enviando comando..."";

            fetch("/api/move", {
                method: "POST",
                headers: { "Content-Type": "text/html" },
                body: direction
            })
            .then(r => r.json())
            .then(data => {
                document.getElementById("status").innerText = "Estado: "" + data.status;
            })
            .catch(() => {
                document.getElementById("status").innerText = "Estado: Error de conexión";
            });
        }
    </script>
</body>
</html>

Para acceder a la API, el dispositivo primero se debe conectar a la red Wi-Fi llamada robot test, y luego acceder a 192.168.4.1:8080/index.html.