gioser

tawasuyu/brahman

Rust 496TOML 195Markdown 55JSON 197001 Sutichaykuna

feat(renaser): Fase 19 — voz del userspace hacia la red (pregon)

Tres capacidades nuevas en wasm/env (12-14):

- sys_net_mac(salida) -> i32: escribe los seis bytes del MAC del
  dispositivo. 0 OK, -1 si no hay red.
- sys_net_enviar(ptr, len) -> i32: envia un frame Ethernet crudo.
  Valida rango contra la memoria lineal del modulo.
- sys_net_recibir(salida, capacidad) -> i32: drena UN paquete por
  llamada hacia el buffer del modulo. Devuelve los bytes copiados, 0
  si nada pendiente, codigos negativos diagnosticos.

Añadida red::recibir_en(buf) -> usize como su contraparte del driver:
gemelo cooperativo de drenar_rx que aterriza en un buffer del usuario.

App nueva pregon (apps/pregon/, 4.2 KiB WASM): lienzo 480x160, tipografia
8x8 (font8x8) escalada x2. Al init pide su MAC y anuncia su presencia
con un broadcast Ethernet — destino FF:FF:FF:FF:FF:FF, EtherType
experimental 0x88B5, payload ASCII 'renaser :: hola desde mi red'. En
cada tick drena un paquete con sys_net_recibir y muestra el titulo, el
MAC propio, las cuentas TX/RX, y los datos del ultimo frame entrante.

GENESIS 8 -> 9 apps (pregon en posicion 2 detras de bitacora);
CELDA_TASKBAR_ANCHO 130 -> 116 px para que las nueve pestañas + lanzador
+ reloj caben holgadas en 1280 px.

tarea_red del kernel ya no drena RX (la cola pertenece al userspace),
conserva solo el envio del ARP de prueba al arrancar.

Verificada en QEMU con -object filter-dump. El pcap captura tres frames
en orden: (1) broadcast 88B5 de pregon con su payload, (2) ARP request
del kernel, (3) ARP reply del gateway 52:55:0a:00:02:02. La consola
anuncia 'manifiesto :: 9 apps nacidas del grafo'.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

07ab095d4202aa5ff30d9a109e610d517a434e74 · ruwaq sergio · (5 killa ñawpaq) · tayta b1be94e7c · kay commitpi sach'ata qhaway

Semántico sayk'uy · 32

renaser/apps/pregon/src/lib.rs

renaser/boot/src/main.rs

  • silq'uyninpi t'ikrakurqan def GENESIS

renaser/kernel/src/compositor.rs

renaser/kernel/src/drivers/red.rs

renaser/kernel/src/main.rs

renaser/kernel/src/wasm/env.rs

13 qillqakuna · +586 −39

diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md
index d88a7fc..6e9b7b5 100644
--- a/renaser/CHANGELOG.md
+++ b/renaser/CHANGELOG.md
@@ -1156,3 +1156,69 @@ el kernel abre ahora una boca y una oreja al exterior: una tarjeta de red.
   El src del paquete entrante codifica la IP del gateway dentro del MAC
   (`52:55:0a:00:02:02` = QEMU prefix + `10.0.2.2`): la respuesta ARP es
   legítima. Renaser ya habla con la red.
+
+---
+
+## Fase 19 — Voz del userspace hacia la red — 2026-05-23
+
+La Fase 18 hizo que el kernel hablara con la red. La Fase 19 le da la
+misma capacidad a los apps: tres capacidades nuevas (`sys_net_mac`,
+`sys_net_enviar`, `sys_net_recibir`) y una primera app que las usa.
+
+### Añadido
+- **`drivers::red::recibir_en(buf) -> usize`** — recibe UN paquete por
+  llamada, lo copia al buffer del llamante, recicla el descriptor a la
+  cola RX. Devuelve 0 si no hay paquete pendiente. El gemelo cooperativo
+  de `drenar_rx`, pensado para la interfaz que el host expone a los apps.
+- **Tres capacidades nuevas en `wasm/env`** (índices 12, 13, 14):
+  - `sys_net_mac(salida: u32) -> i32` — escribe seis bytes con la MAC del
+    dispositivo en `(salida, 6)`. Devuelve `0` OK o `-1` si no hay red.
+  - `sys_net_enviar(ptr: u32, len: u32) -> i32` — envía el frame Ethernet
+    crudo. Devuelve `0` OK, `-1` red caída, `-2` longitud inválida,
+    `-3` rango fuera de la memoria lineal.
+  - `sys_net_recibir(salida: u32, capacidad: u32) -> i32` — drena UN
+    paquete (si hay) hacia `(salida, capacidad)`. Devuelve los bytes
+    copiados, `0` si nada pendiente, `-1` si no hay red, `-2` si la
+    capacidad excede la memoria lineal.
+  Todas validan `rango()` contra la memoria del módulo y registran
+  diagnóstico por COM1 si falla la validación.
+- **App `pregon` (`apps/pregon/`, 4.2 KiB WASM)** — la primera voz del
+  userspace hacia la red. Al arrancar:
+  1. Pide su MAC al host vía `sys_net_mac`.
+  2. Si la tiene, anuncia su presencia con un broadcast Ethernet
+     (destino `FF:FF:FF:FF:FF:FF`, EtherType experimental `0x88B5`,
+     payload ASCII `"renaser :: hola desde mi red"` — 42 bytes en el cable).
+  En cada `tick` drena un paquete con `sys_net_recibir` y pinta:
+  el título, el MAC propio, las cuentas TX/RX, y los datos del último
+  frame entrante (tamaño, EtherType, src MAC) — todo en tipografía 8×8
+  embebida (crate `font8x8`), escalada x2 sobre un lienzo 480×160 índigo.
+- **`pregon` en el manifiesto de genesis** (`boot/src/main.rs`): la novena
+  app, posición 2 (detrás de `bitacora`). El disco se siembra ahora con
+  9 entradas; el kernel reporta `manifiesto :: 9 apps nacidas del grafo`.
+
+### Cambiado
+- **`tarea_red`** del kernel ya no drena la cola RX: la posesión de la
+  cola pasa al userspace vía `sys_net_recibir`. La tarea conserva el
+  envío del ARP de prueba al gateway en cuanto el dispositivo se
+  estabiliza, y termina.
+- **`CELDA_TASKBAR_ANCHO`** baja de 130 a 116 px para que las nueve
+  pestañas + lanzador + reloj caben holgadas en 1280 px.
+- **`ETHER_TYPE_RENASER`** del driver pasa a `#[allow(dead_code)]` —
+  ahora vive en el userspace (en `pregon`); el kernel la conserva como
+  referencia para futuras pilas nativas.
+
+### Verificado
+- QEMU headless con `-object filter-dump,id=fd,netdev=net0,file=/tmp/renaser.pcap`.
+  El pcap captura tres frames en orden:
+  ```
+  #1 incl=42 dst=ff:ff:ff:ff:ff:ff src=52:54:00:12:34:56 etype=0x88b5
+     payload: 'renaser :: hola desde mi red'
+  #2 incl=42 dst=ff:ff:ff:ff:ff:ff src=52:54:00:12:34:56 etype=0x0806 (ARP REQ)
+  #3 incl=64 dst=52:54:00:12:34:56 src=52:55:0a:00:02:02 etype=0x0806 (ARP REP)
+  ```
+  El primero es `pregon`; el segundo el kernel; el tercero es el gateway
+  respondiéndonos — el ciclo completo de salida y entrada en una sola
+  fase.
+- Captura de pantalla: la ventana `pregon :: voz hacia la red` aparece
+  teselada, su pestaña sale en la barra de tareas entre `bitacora` y
+  `tonada`, y la consola anuncia `manifiesto :: 9 apps nacidas del grafo`.
diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md
index 823a4a1..d78cc73 100644
--- a/renaser/CLAUDE.md
+++ b/renaser/CLAUDE.md
@@ -28,7 +28,7 @@ Reconstruir una app WASM del userspace tras tocarla. Los `.wasm` viven en
 modulo `hello_wasm` se copia como `app.wasm`, el resto conserva su nombre:
 
 ```sh
-cd apps/<app>            # hello_wasm, discola, glotona, cronista, memoriosa, pulso, tonada, bitacora
+cd apps/<app>            # hello_wasm, discola, glotona, cronista, memoriosa, pulso, tonada, bitacora, pregon
 cargo build --target wasm32-unknown-unknown --release
 cp target/wasm32-unknown-unknown/release/<app>.wasm ../../kernel/assets/<app>.wasm
 # (hello_wasm es la excepcion: su destino es kernel/assets/app.wasm)
@@ -90,9 +90,12 @@ arrancar, repique al lanzar o cerrar, bajo al desalojar, con prioridad
 sobre `sys_tono`— la Fase 16 COMPLETA —la barra viva: botón «+»
 lanzador a la izquierda y reloj `mm:ss` a la derecha que late cada
 segundo— la Fase 17 COMPLETA —`bitacora`, editor de texto que persiste entre
-arranques en el grafo de objetos (tipografía 8×8 embebida)— y la Fase 18
+arranques en el grafo de objetos (tipografía 8×8 embebida)— la Fase 18
 COMPLETA —red: virtio-net + ARP al gateway de QEMU + recepción de
-paquetes registrada por COM1—. Todo verificado en QEMU. Ver `ROADMAP.md`.
+paquetes registrada por COM1— y la Fase 19 COMPLETA —voz del userspace
+hacia la red: capacidades `sys_net_mac` / `sys_net_enviar` / `sys_net_recibir`
++ la app `pregon` que pregona su presencia con un broadcast Ethernet
+y muestra el tráfico entrante—. Todo verificado en QEMU. Ver `ROADMAP.md`.
 
 ## Flujo de trabajo
 
diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md
index b1ee7ad..aff99c2 100644
--- a/renaser/DIARIO.md
+++ b/renaser/DIARIO.md
@@ -584,4 +584,28 @@ sola.
 
 ---
 
+## La voz pasa a los inquilinos — el pregón
+
+Ayer la casa abrió la puerta lateral y saludó. Hoy ese saludo deja de ser
+suyo y empieza a ser de quien la habita. Antes, sólo el conserje —el
+kernel— podía asomarse a la red. Hoy le entrega tres llaves a los
+inquilinos: una para preguntar «¿cuál es mi dirección?», otra para
+gritar algo, otra para recoger lo que entra. Llaves pequeñas, una a la
+vez, pero suficientes.
+
+Y para estrenarlas nace un huésped nuevo, *pregón*. Su único oficio es
+ese: pregonar. Apenas despierta, mira su credencial, se asoma a la
+puerta y suelta su grito —«renaser :: hola desde mi red»— para que
+quien quiera lo escuche. Luego se queda en su ventana, esperando lo
+que llegue, y en cuanto algo entra lo apunta en su pizarra: cuántas
+letras, de quién, qué tipo de cosa. Una bitácora viva, en tiempo real,
+del tráfico que entra y sale.
+
+Si uno graba lo que pasa por la red mientras la casa despierta,
+encuentra exactamente tres notas: el pregón del huésped, la pregunta
+del conserje, la respuesta del vecino. Tres frases bastan para mostrar
+que la voz ya no es de uno solo. Ahora la casa habla en coro.
+
+---
+
 *El diario continúa. La próxima página la escribirá la próxima jornada.*
diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md
index fef51bb..a47af82 100644
--- a/renaser/ROADMAP.md
+++ b/renaser/ROADMAP.md
@@ -323,6 +323,40 @@ Líneas abiertas posteriores: capacidades `sys_net_*` para que los apps
 también hablen; una pila mínima ARP/IP/UDP; reciclado de las ranuras de
 ventana cerradas; audio con varias voces (PCM).
 
+## Fase 19 — voz del userspace hacia la red (completada)
+
+La Fase 18 hizo que el kernel hablara con la red. La Fase 19 le da la
+misma capacidad a los apps con tres capacidades nuevas, y estrena la
+primera app que las usa.
+
+- `drivers::red::recibir_en(buf) -> usize`: drena UN paquete por
+  llamada, recicla el descriptor a la cola RX. El gemelo cooperativo de
+  `drenar_rx`, dimensionado para el patrón que los apps usan.
+- Tres capacidades `wasm/env` nuevas: `sys_net_mac` (escribe la MAC en
+  un buffer del módulo), `sys_net_enviar(ptr, len)` (envía un frame
+  Ethernet crudo) y `sys_net_recibir(salida, capacidad)` (drena un
+  paquete, devuelve los bytes copiados). Todas validan rango contra la
+  memoria lineal del módulo y devuelven códigos negativos diagnósticos.
+- App `pregon` (4.2 KiB WASM): al arrancar pide su MAC, anuncia su
+  presencia con un broadcast Ethernet (EtherType `0x88B5`, payload
+  `"renaser :: hola desde mi red"`) y en cada `tick` drena un paquete y
+  lo muestra en su lienzo 480×160 — título, MAC, cuentas TX/RX y datos
+  del último frame entrante (tamaño, EtherType, src MAC).
+- `tarea_red` ya no drena RX: la posesión de la cola pasa al userspace.
+  El kernel conserva sólo el envío del ARP de prueba al arrancar.
+- `GENESIS` crece de 8 a 9 apps; `pregon` queda en posición 2 (detrás
+  de `bitacora`). `CELDA_TASKBAR_ANCHO` baja de 130 a 116 px para que
+  las nueve pestañas + lanzador + reloj caben en 1280 px.
+
+Verificada con `-object filter-dump,...,file=/tmp/renaser.pcap`. El
+pcap captura tres frames: el broadcast `0x88B5` de `pregon`, el ARP
+request del kernel y el ARP reply del gateway. El primero es el del
+huésped — la voz del userspace ya pasó al cable.
+
+Líneas abiertas posteriores: una pila mínima ARP/IP/UDP para los apps;
+un servidor de eco / un cliente DNS de juguete; reciclado de las
+ranuras de ventana cerradas; audio con varias voces (PCM).
+
 ## Principios que persisten entre fases
 
 - Reutilizar infraestructura madura de la comunidad antes que reinventar.
diff --git a/renaser/apps/pregon/Cargo.lock b/renaser/apps/pregon/Cargo.lock
new file mode 100644
index 0000000..a6b52f4
--- /dev/null
+++ b/renaser/apps/pregon/Cargo.lock
@@ -0,0 +1,16 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "font8x8"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e"
+
+[[package]]
+name = "pregon"
+version = "0.1.0"
+dependencies = [
+ "font8x8",
+]
diff --git a/renaser/apps/pregon/Cargo.toml b/renaser/apps/pregon/Cargo.toml
new file mode 100644
index 0000000..18750a2
--- /dev/null
+++ b/renaser/apps/pregon/Cargo.toml
@@ -0,0 +1,30 @@
+# =============================================================================
+#  renaser :: apps/pregon — Fase 19 :: el primer app que sale a la red
+# -----------------------------------------------------------------------------
+#  Pregona la presencia de renaser en la red con un frame Ethernet broadcast,
+#  y pone en pantalla lo que va sucediendo: su MAC, paquetes enviados y
+#  recibidos, y los primeros bytes del ultimo frame entrante. Demuestra las
+#  capacidades `sys_net_*` de la Fase 19.
+# =============================================================================
+
+[package]
+name = "pregon"
+version = "0.1.0"
+edition = "2021"
+description = "renaser :: app WASM — voz hacia la red, demo de sys_net_*"
+
+[workspace]
+
+[dependencies]
+font8x8 = { version = "0.3", default-features = false, features = ["unicode"] }
+
+[lib]
+crate-type = ["cdylib"]
+
+[profile.dev]
+panic = "abort"
+
+[profile.release]
+panic = "abort"
+opt-level = "s"
+lto = true
diff --git a/renaser/apps/pregon/src/lib.rs b/renaser/apps/pregon/src/lib.rs
new file mode 100644
index 0000000..1031961
--- /dev/null
+++ b/renaser/apps/pregon/src/lib.rs
@@ -0,0 +1,276 @@
+// =============================================================================
+//  renaser :: apps/pregon — Fase 19 :: voz del userspace hacia la red
+// -----------------------------------------------------------------------------
+//  La Fase 18 hizo que el kernel pudiera hablar con la red; la Fase 19 le da la
+//  misma capacidad a los apps via `sys_net_*`. `pregon` es el primer app que
+//  la usa: al arrancar pide su MAC al host, anuncia su presencia con un frame
+//  Ethernet broadcast (EtherType experimental 0x88B5) y, en cada `tick`, drena
+//  un paquete de la cola RX y lo muestra en pantalla.
+//
+//  El kernel envia tambien al arrancar un ARP request al gateway de QEMU
+//  (Fase 18); `pregon` recibira su respuesta como uno de los primeros paquetes
+//  RX — visible aqui en la linea «ultimo».
+// =============================================================================
+
+#![no_std]
+
+use font8x8::legacy::BASIC_LEGACY;
+
+#[link(wasm_import_module = "renaser")]
+extern "C" {
+    fn sys_render_frame(ptr: u32, len: u32);
+    fn sys_net_mac(salida: u32) -> i32;
+    fn sys_net_enviar(ptr: u32, len: u32) -> i32;
+    fn sys_net_recibir(salida: u32, capacidad: u32) -> i32;
+}
+
+#[panic_handler]
+fn al_fallar(_: &core::panic::PanicInfo) -> ! {
+    loop {}
+}
+
+// --- Geometria del lienzo ----------------------------------------------------
+
+const ANCHO: usize = 480;
+const ALTO: usize = 160;
+const PASO: usize = 16;
+const MARGEN: usize = 16;
+
+const FONDO: u32 = 0x0A_18_30;
+const TINTA: u32 = 0xE8_EC_F4;
+const TINTA_TENUE: u32 = 0x6A_70_80;
+const ETIQUETA: u32 = 0x8B_5C_F6;
+
+// --- Constantes del paquete que enviamos ------------------------------------
+
+const ETHER_TYPE_RENASER: u16 = 0x88B5;
+const MENSAJE: &[u8; 28] = b"renaser :: hola desde mi red";
+const FRAME_LEN: usize = 14 + 28;
+
+// --- Estado del app ---------------------------------------------------------
+
+static mut MAC: [u8; 6] = [0; 6];
+static mut MAC_OK: bool = false;
+static mut TX: u32 = 0;
+static mut RX: u32 = 0;
+static mut BUFFER_RX: [u8; 1600] = [0; 1600];
+static mut ULTIMA_LEN: usize = 0;
+static mut LIENZO: [u32; ANCHO * ALTO] = [0; ANCHO * ALTO];
+
+// --- ABI del userspace ------------------------------------------------------
+
+#[no_mangle]
+pub extern "C" fn init() {
+    // 1. Pedir la MAC al host. -1 significa «no hay red montada».
+    let mac = unsafe { &mut *core::ptr::addr_of_mut!(MAC) };
+    // SEGURIDAD: `sys_net_mac` escribe seis bytes en (ptr, 6); el host lo valida.
+    let r = unsafe { sys_net_mac(mac.as_mut_ptr() as u32) };
+    if r == 0 {
+        unsafe {
+            MAC_OK = true;
+        }
+        // 2. Anunciar la presencia con un broadcast Ethernet de EtherType
+        //    experimental + un mensaje legible. El payload aparece tal cual en
+        //    cualquier pcap, sin descifrar nada.
+        let mut frame = [0u8; FRAME_LEN];
+        frame[0..6].copy_from_slice(&[0xff; 6]); // destino: broadcast
+        frame[6..12].copy_from_slice(mac);
+        frame[12..14].copy_from_slice(&ETHER_TYPE_RENASER.to_be_bytes());
+        frame[14..].copy_from_slice(MENSAJE);
+        // SEGURIDAD: `sys_net_enviar` lee (ptr, len) de nuestra memoria.
+        let r = unsafe { sys_net_enviar(frame.as_ptr() as u32, frame.len() as u32) };
+        if r == 0 {
+            unsafe {
+                TX += 1;
+            }
+        }
+    }
+    pintar();
+}
+
+#[no_mangle]
+pub extern "C" fn tick() {
+    // Drenar UN paquete por tick — si la cola RX tiene mas, se atendera en
+    // las proximas vueltas. Cero coste cuando no hay nada que leer.
+    let buf = unsafe { &mut *core::ptr::addr_of_mut!(BUFFER_RX) };
+    let n = unsafe { sys_net_recibir(buf.as_mut_ptr() as u32, buf.len() as u32) };
+    if n > 0 {
+        unsafe {
+            RX += 1;
+            ULTIMA_LEN = n as usize;
+        }
+    }
+    pintar();
+}
+
+// --- Renderizado ------------------------------------------------------------
+
+fn pintar() {
+    let lienzo = unsafe { &mut *core::ptr::addr_of_mut!(LIENZO) };
+    for p in lienzo.iter_mut() {
+        *p = FONDO;
+    }
+
+    // Titulo + subrayado.
+    pintar_texto(lienzo, b"pregon :: voz hacia la red", MARGEN, 8, ETIQUETA);
+    let y_linea = 8 + PASO + 4;
+    for x in MARGEN..(ANCHO - MARGEN) {
+        lienzo[y_linea * ANCHO + x] = ETIQUETA;
+        lienzo[(y_linea + 1) * ANCHO + x] = ETIQUETA;
+    }
+
+    let mac_ok = unsafe { MAC_OK };
+    if !mac_ok {
+        pintar_texto(lienzo, b"sin red -- no hay tarjeta montada", MARGEN, 48, TINTA);
+        volcar(lienzo);
+        return;
+    }
+
+    let mac = unsafe { &*core::ptr::addr_of!(MAC) };
+    let tx = unsafe { TX };
+    let rx = unsafe { RX };
+
+    // Linea MAC: "mac: xx:xx:xx:xx:xx:xx"
+    {
+        let mut linea = [b' '; 22];
+        linea[0..5].copy_from_slice(b"mac: ");
+        let mut p = 5;
+        for (i, &b) in mac.iter().enumerate() {
+            let par = hex_byte(b);
+            linea[p..p + 2].copy_from_slice(&par);
+            p += 2;
+            if i < 5 {
+                linea[p] = b':';
+                p += 1;
+            }
+        }
+        pintar_texto(lienzo, &linea, MARGEN, 44, TINTA);
+    }
+
+    // Linea TX/RX: "tx: N    rx: M"
+    {
+        let mut linea = [b' '; 20];
+        linea[0..4].copy_from_slice(b"tx: ");
+        let mut buf = [0u8; 10];
+        let s = dec(tx, &mut buf);
+        linea[4..4 + s.len()].copy_from_slice(s);
+        let p = 4 + s.len() + 4;
+        if p + 4 <= linea.len() {
+            linea[p..p + 4].copy_from_slice(b"rx: ");
+            let mut buf2 = [0u8; 10];
+            let s2 = dec(rx, &mut buf2);
+            let q = p + 4;
+            if q + s2.len() <= linea.len() {
+                linea[q..q + s2.len()].copy_from_slice(s2);
+            }
+        }
+        pintar_texto(lienzo, &linea, MARGEN, 72, TINTA);
+    }
+
+    // Linea «ultimo»: cuantos bytes, tipo y src.
+    let len = unsafe { ULTIMA_LEN };
+    if len >= 14 {
+        let buf = unsafe { &*core::ptr::addr_of!(BUFFER_RX) };
+        let etype = u16::from_be_bytes([buf[12], buf[13]]);
+        // Linea 1: "ultimo: N bytes  type=0x????"
+        let mut linea = [b' '; 30];
+        linea[0..8].copy_from_slice(b"ultimo: ");
+        let mut dbuf = [0u8; 10];
+        let s = dec(len as u32, &mut dbuf);
+        linea[8..8 + s.len()].copy_from_slice(s);
+        let mut p = 8 + s.len();
+        linea[p..p + 8].copy_from_slice(b" bytes  ");
+        p += 8;
+        if p + 7 <= linea.len() {
+            linea[p..p + 7].copy_from_slice(b"type=0x");
+            p += 7;
+            let hi = hex_byte((etype >> 8) as u8);
+            let lo = hex_byte((etype & 0xff) as u8);
+            if p + 4 <= linea.len() {
+                linea[p..p + 2].copy_from_slice(&hi);
+                linea[p + 2..p + 4].copy_from_slice(&lo);
+            }
+        }
+        pintar_texto(lienzo, &linea, MARGEN, 104, TINTA_TENUE);
+
+        // Linea 2: "src: xx:xx:xx:xx:xx:xx"
+        let src = &buf[6..12];
+        let mut linea2 = [b' '; 22];
+        linea2[0..5].copy_from_slice(b"src: ");
+        let mut p = 5;
+        for (i, &b) in src.iter().enumerate() {
+            let par = hex_byte(b);
+            linea2[p..p + 2].copy_from_slice(&par);
+            p += 2;
+            if i < 5 {
+                linea2[p] = b':';
+                p += 1;
+            }
+        }
+        pintar_texto(lienzo, &linea2, MARGEN, 132, TINTA);
+    } else {
+        pintar_texto(lienzo, b"esperando primer paquete...", MARGEN, 104, TINTA_TENUE);
+    }
+
+    volcar(lienzo);
+}
+
+fn volcar(lienzo: &[u32]) {
+    // SEGURIDAD: el host valida (ptr, len) contra nuestra memoria lineal.
+    unsafe {
+        sys_render_frame(lienzo.as_ptr() as u32, (ANCHO * ALTO * 4) as u32);
+    }
+}
+
+fn pintar_texto(lienzo: &mut [u32], texto: &[u8], x: usize, y: usize, color: u32) {
+    let mut cx = x;
+    for &c in texto {
+        if cx + PASO > ANCHO {
+            break;
+        }
+        pintar_glifo(lienzo, c, cx, y, color);
+        cx += PASO;
+    }
+}
+
+fn pintar_glifo(lienzo: &mut [u32], c: u8, x: usize, y: usize, color: u32) {
+    let glifo = if (c as usize) < 128 {
+        BASIC_LEGACY[c as usize]
+    } else {
+        BASIC_LEGACY[b'?' as usize]
+    };
+    for row in 0..8 {
+        for col in 0..8 {
+            if glifo[row] & (1 << col) != 0 {
+                let px = x + col * 2;
+                let py = y + row * 2;
+                if px + 1 >= ANCHO || py + 1 >= ALTO {
+                    continue;
+                }
+                lienzo[py * ANCHO + px] = color;
+                lienzo[py * ANCHO + px + 1] = color;
+                lienzo[(py + 1) * ANCHO + px] = color;
+                lienzo[(py + 1) * ANCHO + px + 1] = color;
+            }
+        }
+    }
+}
+
+fn hex_byte(b: u8) -> [u8; 2] {
+    let n = |x: u8| if x < 10 { b'0' + x } else { b'a' + (x - 10) };
+    [n(b >> 4), n(b & 0xf)]
+}
+
+fn dec(mut n: u32, buf: &mut [u8; 10]) -> &[u8] {
+    if n == 0 {
+        buf[0] = b'0';
+        return &buf[..1];
+    }
+    let mut i = 10;
+    while n > 0 {
+        i -= 1;
+        buf[i] = b'0' + (n % 10) as u8;
+        n /= 10;
+    }
+    &buf[i..]
+}
diff --git a/renaser/boot/src/main.rs b/renaser/boot/src/main.rs
index b91762d..0f664df 100644
--- a/renaser/boot/src/main.rs
+++ b/renaser/boot/src/main.rs
@@ -107,14 +107,16 @@ struct AppGenesis {
     region: (u32, u32, u32, u32),
 }
 
-/// El userspace de genesis — las ocho aplicaciones que pueblan un disco recien
-/// forjado. La `bitacora` (Fase 17, editor que persiste), la melodia visual
-/// `tonada` (Fase 12), el compas visual `pulso` (Fase 11), un saludo (`hola`),
-/// la `memoriosa` interactiva que recuerda entre sesiones (Fase 7c), y tres
-/// demos de los guardarrailes del kernel: `discola` (combustible), `glotona`
-/// (memoria) y `cronista` (la cronica de los arranques).
-const GENESIS: [AppGenesis; 8] = [
+/// El userspace de genesis — las nueve aplicaciones que pueblan un disco recien
+/// forjado. La `bitacora` (Fase 17, editor que persiste), el `pregon` (Fase 19,
+/// la primera voz hacia la red), la melodia visual `tonada` (Fase 12), el
+/// compas visual `pulso` (Fase 11), un saludo (`hola`), la `memoriosa`
+/// interactiva que recuerda entre sesiones (Fase 7c), y tres demos de los
+/// guardarrailes del kernel: `discola` (combustible), `glotona` (memoria) y
+/// `cronista` (la cronica de los arranques).
+const GENESIS: [AppGenesis; 9] = [
     AppGenesis { nombre: "bitacora", archivo: "bitacora.wasm", region: (100, 120, 480, 280) },
+    AppGenesis { nombre: "pregon", archivo: "pregon.wasm", region: (100, 120, 480, 160) },
     AppGenesis { nombre: "tonada", archivo: "tonada.wasm", region: (100, 120, 360, 120) },
     AppGenesis { nombre: "pulso", archivo: "pulso.wasm", region: (100, 120, 360, 120) },
     AppGenesis { nombre: "hola", archivo: "app.wasm", region: (100, 120, 480, 560) },
diff --git a/renaser/kernel/assets/pregon.wasm b/renaser/kernel/assets/pregon.wasm
new file mode 100755
index 0000000..0a66914
Binary files /dev/null and b/renaser/kernel/assets/pregon.wasm differ
diff --git a/renaser/kernel/src/compositor.rs b/renaser/kernel/src/compositor.rs
index db2ad60..55922fe 100644
--- a/renaser/kernel/src/compositor.rs
+++ b/renaser/kernel/src/compositor.rs
@@ -68,9 +68,9 @@ const FRANJA_CONSOLA: usize = 296;
 const FRANJA_TASKBAR: usize = 40;
 
 /// Anchura de cada celda de la barra de tareas, en pixeles. Dimensionada para
-/// que las ocho apps de genesis + el lanzador + el reloj caben holgados en una
-/// pantalla de 1280 px.
-const CELDA_TASKBAR_ANCHO: usize = 130;
+/// que las nueve apps de genesis (Fase 19 anexa `pregon`) + el lanzador + el
+/// reloj caben holgados en una pantalla de 1280 px.
+const CELDA_TASKBAR_ANCHO: usize = 116;
 /// Hueco entre celdas adyacentes de la barra.
 const CELDA_TASKBAR_HUECO: usize = 6;
 /// Margen izquierdo y derecho de la barra de tareas.
diff --git a/renaser/kernel/src/drivers/red.rs b/renaser/kernel/src/drivers/red.rs
index 9291b56..83bb051 100644
--- a/renaser/kernel/src/drivers/red.rs
+++ b/renaser/kernel/src/drivers/red.rs
@@ -37,7 +37,10 @@ const PAQUETE_MAX: usize = 1600;
 const PROFUNDIDAD_COLA: usize = 16;
 
 /// EtherType experimental (rango 0x88B5-0x88B6, reservado por IEEE para uso
-/// local). renaser lo usaria si quisiera definir su propio protocolo.
+/// local). renaser lo usaria si quisiera definir su propio protocolo. Desde
+/// la Fase 19 lo usa `pregon` desde el userspace; el kernel lo conserva como
+/// referencia para diagnosticos y futuros protocolos nativos.
+#[allow(dead_code)]
 pub const ETHER_TYPE_RENASER: u16 = 0x88B5;
 /// EtherType de ARP.
 pub const ETHER_TYPE_ARP: u16 = 0x0806;
@@ -190,6 +193,7 @@ pub fn enviar(frame: &[u8]) -> Result<(), &'static str> {
 /// Drena los paquetes RX pendientes y aplica `callback` a cada uno. Cada
 /// bufer se recicla a la cola RX al terminar — el dispositivo tiene siempre
 /// receptores listos para la proxima IRQ.
+#[allow(dead_code)]
 pub fn drenar_rx<F: FnMut(&[u8])>(mut callback: F) {
     let Some(tarjeta) = TARJETA.get() else {
         return;
@@ -211,6 +215,31 @@ pub fn drenar_rx<F: FnMut(&[u8])>(mut callback: F) {
     });
 }
 
+/// Recibe UN paquete: copia su contenido en `buf` y devuelve los bytes
+/// copiados, o `0` si no hay paquete pendiente. La interfaz que el host
+/// expone a los apps via `sys_net_recibir` — un paquete por llamada (Fase 19).
+pub fn recibir_en(buf: &mut [u8]) -> usize {
+    let Some(tarjeta) = TARJETA.get() else {
+        return 0;
+    };
+    interrupts::without_interrupts(|| {
+        let mut tarjeta = tarjeta.lock();
+        if !tarjeta.0.can_recv() {
+            return 0;
+        }
+        let rx = match tarjeta.0.receive() {
+            Ok(r) => r,
+            Err(_) => return 0,
+        };
+        let pkt = rx.packet();
+        let n = pkt.len().min(buf.len());
+        buf[..n].copy_from_slice(&pkt[..n]);
+        let _ = tarjeta.0.recycle_rx_buffer(rx);
+        PAQUETES_RX.fetch_add(1, Ordering::Relaxed);
+        n
+    })
+}
+
 // =============================================================================
 //  Composicion de un ARP request — el primer paquete que renaser saluda
 // =============================================================================
diff --git a/renaser/kernel/src/main.rs b/renaser/kernel/src/main.rs
index f612bb4..c730cdb 100644
--- a/renaser/kernel/src/main.rs
+++ b/renaser/kernel/src/main.rs
@@ -174,9 +174,10 @@ async fn tarea_compositor() {
     }
 }
 
-/// FASE 18 — la prueba viva del enlace de red. Envia un ARP request al
-/// gateway de QEMU (10.0.2.2) y registra por COM1 cada paquete que llegue de
-/// vuelta. El primer "hola" de renaser hacia el exterior.
+/// FASE 18/19 — la primera voz del kernel hacia la red. Envia un ARP request
+/// al gateway de QEMU para anunciarse, y termina: a partir de la Fase 19, los
+/// apps drenan la cola RX por su cuenta via `sys_net_recibir`, asi que el
+/// kernel no le quita paquetes a nadie.
 async fn tarea_red(mac: drivers::red::Mac) {
     // Dejar un par de fotogramas para que la cola RX se estabilice.
     for _ in 0..10 {
@@ -199,27 +200,7 @@ async fn tarea_red(mac: drivers::red::Mac) {
             let _ = writeln!(baliza::Serie, "red :: envio fallido :: {motivo}");
         }
     }
-    // Loop perpetuo: drenar la cola RX y registrar cada paquete en COM1.
-    loop {
-        async_system::reloj::EsperaFrame::nueva().await;
-        drivers::red::drenar_rx(|payload| {
-            if payload.len() < 14 {
-                return;
-            }
-            let etype = u16::from_be_bytes([payload[12], payload[13]]);
-            let src = &payload[6..12];
-            let dst = &payload[0..6];
-            let _ = writeln!(
-                baliza::Serie,
-                "red :: RX {} bytes :: dst={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} \
-                 src={:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} type={:#06x}",
-                payload.len(),
-                dst[0], dst[1], dst[2], dst[3], dst[4], dst[5],
-                src[0], src[1], src[2], src[3], src[4], src[5],
-                etype,
-            );
-        });
-    }
+    // La tarea termina aqui. Los apps se encargan del trafico desde ahora.
 }
 
 /// FASE 6.2 — la prueba viva de la E/S asincrona. Esta tarea del reactor lee el
diff --git a/renaser/kernel/src/wasm/env.rs b/renaser/kernel/src/wasm/env.rs
index f7efa45..66763d2 100644
--- a/renaser/kernel/src/wasm/env.rs
+++ b/renaser/kernel/src/wasm/env.rs
@@ -15,7 +15,10 @@
 //    * sys_estado_cargar     — leer el estado persistido de la app (Fase 7c);
 //    * sys_estado_guardar    — anclar el estado persistido de la app (Fase 7c);
 //    * sys_tiempo_mono       — leer el reloj monotono del sistema (Fase 11);
-//    * sys_tono              — hacer sonar la bocina del PC (Fase 12).
+//    * sys_tono              — hacer sonar la bocina del PC (Fase 12);
+//    * sys_net_mac           — leer la MAC de la tarjeta de red (Fase 19);
+//    * sys_net_enviar        — enviar un frame Ethernet crudo (Fase 19);
+//    * sys_net_recibir       — leer el siguiente frame recibido (Fase 19).
 //
 //  GUARDARRAIL: el kernel valida MATEMATICAMENTE todo puntero que el modulo le
 //  entrega contra los limites reales de su memoria lineal. No se confia en que
@@ -482,5 +485,88 @@ pub(crate) fn enlazar_capacidades(
         },
     )?;
 
+    // --- CAPACIDAD 12 :: sys_net_mac(salida) -> i32 ---
+    // Copia los 6 bytes de la MAC de la tarjeta de red en `salida`. Devuelve 0
+    // si la red esta montada; -1 si no hay tarjeta o aun no se monto.
+    enlazador.func_wrap(
+        "renaser",
+        "sys_net_mac",
+        |mut caller: Caller<'_, ContextoCapacidades>, salida: u32| -> Result<i32, Error> {
+            let Some(mac) = crate::drivers::red::mac() else {
+                return Ok(-1);
+            };
+            let memoria = obtener_memoria(&caller)?;
+            {
+                let m = memoria.data(&caller);
+                rango(m, salida, 6, "WASM :: sys_net_mac desbordo la memoria lineal")?;
+            }
+            let m = memoria.data_mut(&mut caller);
+            m[salida as usize..salida as usize + 6].copy_from_slice(&mac);
+            Ok(0)
+        },
+    )?;
+
+    // --- CAPACIDAD 13 :: sys_net_enviar(ptr, len) -> i32 ---
+    // Envia un frame Ethernet crudo (cabecera + payload, sin CRC). El app
+    // construye el frame entero en su memoria lineal. Devuelve 0 si el
+    // envio se entrego al dispositivo; -1 si fallo el envio o no hay red.
+    enlazador.func_wrap(
+        "renaser",
+        "sys_net_enviar",
+        |caller: Caller<'_, ContextoCapacidades>, ptr: u32, len: u32| -> Result<i32, Error> {
+            let memoria = obtener_memoria(&caller)?;
+            let datos = memoria.data(&caller);
+            let frame = rango(
+                datos,
+                ptr,
+                len as usize,
+                "WASM :: sys_net_enviar desbordo la memoria lineal",
+            )?;
+            match crate::drivers::red::enviar(frame) {
+                Ok(()) => Ok(0),
+                Err(_) => Ok(-1),
+            }
+        },
+    )?;
+
+    // --- CAPACIDAD 14 :: sys_net_recibir(salida, capacidad) -> i32 ---
+    // Saca el siguiente frame de la cola RX del dispositivo y lo copia en
+    // `salida`. Devuelve los bytes copiados (>0), 0 si no hay frame pendiente,
+    // o -1 si no hay red montada. La cola RX es del dispositivo y se comparte
+    // entre los apps: el primero que pregunte se lleva el paquete.
+    enlazador.func_wrap(
+        "renaser",
+        "sys_net_recibir",
+        |mut caller: Caller<'_, ContextoCapacidades>,
+         salida: u32,
+         capacidad: u32|
+         -> Result<i32, Error> {
+            if crate::drivers::red::mac().is_none() {
+                return Ok(-1);
+            }
+            let memoria = obtener_memoria(&caller)?;
+            // Verificar que el destino cabe ANTES de tocar la cola.
+            {
+                let m = memoria.data(&caller);
+                rango(
+                    m,
+                    salida,
+                    capacidad as usize,
+                    "WASM :: sys_net_recibir desbordo la memoria lineal",
+                )?;
+            }
+            // Bufer kernel-side donde el driver vuelca el frame; luego se copia
+            // a la memoria del app en una sola pasada.
+            let mut buf: alloc::vec::Vec<u8> = alloc::vec![0u8; capacidad as usize];
+            let n = crate::drivers::red::recibir_en(&mut buf);
+            if n == 0 {
+                return Ok(0);
+            }
+            let m = memoria.data_mut(&mut caller);
+            m[salida as usize..salida as usize + n].copy_from_slice(&buf[..n]);
+            Ok(n as i32)
+        },
+    )?;
+
     Ok(())
 }