diff --git a/CLAUDE.md b/CLAUDE.md index d53b1b8..e71fafa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # QTorres — Contexto para Claude Code -> Última actualización: 2026-03-31 +> Última actualización: 2026-04-20 ## Reglas absolutas — NUNCA violar @@ -126,13 +126,16 @@ QTorres/ - ~~#64 FP como ventana maestra~~ ✅ (FP=blocking master, BD=no-wait slave, Ctrl+E toggle, títulos sincronizados, current-file en app-model) - ~~#65 Scroll en BD y FP~~ ✅ (ventanas fijas 900x600, scroll wheel + click scrollbar, límites por contenido real) +**Fase 4 — Hardware (en curso):** +- ~~#19 TCP/IP — bloques básicos cliente~~ ✅ (tcp-open/write/read/close estilo LabVIEW, session-through por connection refnum, verificado con socat) + **Fase 5 — UX y gestión de proyectos (planificado):** - Splash / Welcome screen (Create New VI, Open Existing, proyectos recientes) - Project Explorer con formato .qproj (árbol de ficheros, gestión de dependencias) - Depende de: .qlib (#18) ✅ y FP como ventana maestra (#64) ✅ - **Nota:** Prototipo temprano de `.qproj` existe en `examples/ejemplo.qproj` — sirve como referencia del formato, pero sin tooling de Project Explorer aún -**Próximo paso:** Fase 4 (hardware) → Fase 4.5 (integración red-sg) → Fase 5 (UX) +**Próximo paso:** seguir Fase 4 (#20 USBTMC, #21 Serie, #22 Modbus/servidor TCP, #23 DAQ) → Fase 4.5 (integración red-sg) → Fase 5 (UX) **Refactor 4B ✅ COMPLETADO (2026-04-17):** `compiler.red` (1255 → 18 líneas orquestador + 5 módulos) y `file-io.red` (939 → 17 líneas orquestador + 4 módulos). Todos los módulos <400 líneas excepto `file-io-serialize.red` (468) por `format-qvi` monolítica. 482/482 tests PASS. Ver `docs/refactor-4b-plan.md` para el plan original. @@ -294,13 +297,15 @@ Spec visual: cada tipo implementa su aspecto según `docs/visual-spec.md`. - #64 FP como ventana maestra — BD bajo demanda (Ctrl+E) ✅ - ~~#65 Scroll en BD y FP~~ ✅ (ventanas fijas 900x600, scrollbars draw-based, límites por contenido) -**Fase 4 — Hardware:** -- #19 SCPI sobre TCP/IP (Keysight por red) -- #20 SCPI sobre USB/USBTMC (Keysight por USB) +**Fase 4 — Hardware (en curso):** +- ~~#19 TCP/IP — bloques básicos cliente~~ ✅ (tcp-open/write/read/close estilo LabVIEW, session-through por connection refnum) +- #20 USBTMC — acceso genérico a instrumentos USB (/dev/usbtmc*) - #21 Puerto serie RS-232/RS-485 (Arduino, ESP32) -- #22 TCP/IP genérico (Modbus TCP, protocolos propios) +- #22 Modbus TCP y servidor TCP/IP (depende de #19) - #23 DAQ analógico (comedi/libcomedi) +> **Nota:** NO se implementan bloques SCPI específicos. SCPI es un protocolo de comandos en texto que se envía como string vía `tcp-write` o `usbtmc-write`. Esto mantiene QTorres genérico y sirve también para Modbus, protocolos propios y cualquier otro protocolo sobre TCP/USB. + **Fase 5 — UX y gestión de proyectos:** - Splash / Welcome screen (Create New VI, Open Existing, proyectos recientes) - Project Explorer con formato .qproj (árbol de proyecto, gestión de dependencias) @@ -403,6 +408,7 @@ Cubre sintaxis core, View, Draw, VID, Parse, patrones idiomáticos y gotchas. - Riesgos conocidos: `docs/retos.md` - Bugs GTK Linux: `docs/GTK_ISSUES.md` - **Arquitectura LabVIEW:** `docs/labview-comportamiento.md` — **leer antes de tomar decisiones sobre renderizado de widgets, modos edit/run, o controles custom** +- **TCP/IP API:** `docs/tcp-api.md` — API nativa para Fase 4 (instrumentación por red, Modbus TCP, protocolos propios) ## Problemas conocidos de arquitectura diff --git a/docs/ai-reference.md b/docs/ai-reference.md index 523d383..287070f 100644 --- a/docs/ai-reference.md +++ b/docs/ai-reference.md @@ -395,4 +395,4 @@ qvi-diagram: [ ## Nota sobre evolución -Este documento refleja el estado actual de QTorres (tipos numéricos). Conforme evolucione, se añadirán tipos de datos (`'boolean`, `'string`, `'array`), estructuras de control (loops, case), protocolos de hardware (Modbus, SCPI, MQTT), y nuevos bloques primitivos. +Este documento refleja el estado actual de QTorres (tipos numéricos). Conforme evolucione, se añadirán tipos de datos (`'boolean`, `'string`, `'array`), estructuras de control (loops, case), bloques genéricos de hardware (TCP/IP, USBTMC, serie, Modbus TCP), y nuevos bloques primitivos. diff --git a/docs/decisiones.md b/docs/decisiones.md index 3d5ee1b..7fa67dd 100644 --- a/docs/decisiones.md +++ b/docs/decisiones.md @@ -1031,7 +1031,7 @@ Cuando se implementen comunicaciones con hardware, se activa el error cluster co - Puertos `error-in` y `error-out` visibles en los nodos que lo soporten - Wire de error con color propio (amarillo, como en LabVIEW) - El compilador genera código que chequea el error antes de ejecutar cada nodo -- Los nodos de hardware (SCPI, serial, TCP) siempre tienen puertos de error +- Los nodos de hardware (TCP, USBTMC, serial) siempre tienen puertos de error **Estructura del error cluster:** ```red @@ -1048,13 +1048,13 @@ error-cluster: context [ ```red ; El compilador genera checks de error entre nodos _err: copy error-empty -_err: scpi-write instrument "*IDN?" _err +_err: tcp-write-block "*IDN?" _err if _err/status [ ; saltar nodos dependientes ind_1-face/text: rejoin ["ERROR: " _err/message] exit ; o equivalente según el contexto ] -_err: scpi-read instrument _err +_err: tcp-read-block 256 _err ``` **Razones de la implementación progresiva:** diff --git a/docs/plan.md b/docs/plan.md index 3f928c9..d7d3df5 100644 --- a/docs/plan.md +++ b/docs/plan.md @@ -7,7 +7,7 @@ QTorres es una alternativa open source a LabVIEW para el mismo público objetivo **Principios de diseño:** - Mismo modelo mental que LabVIEW: Front Panel + Block Diagram, dataflow, sub-VIs - Identidad visual propia y más moderna (no un clon visual de LabVIEW) -- Hardware como ciudadano de primera clase: SCPI/VISA para Keysight, serie para microcontroladores, DAQ +- Hardware como ciudadano de primera clase: TCP/IP y USBTMC para instrumentación, serie para microcontroladores, DAQ - Sin dependencias externas. Un binario, multiplataforma. **Por qué puede competir con LabVIEW:** @@ -180,11 +180,12 @@ Los controles de entrada se convierten en `field` editables. Los indicadores de Esta fase es esencial para el público objetivo (mismo que LabVIEW: ingeniería de test y automatización). -### SCPI para instrumentos Keysight y compatibles -- [ ] SCPI sobre TCP/IP (puerto 5025): bloques connect/write/query/close (#28) -- [ ] SCPI sobre USB/USBTMC (/dev/usbtmc*): mismos bloques, diferente transporte (#29) -- [ ] Gestión de errores de instrumento (+/-OPC, error queue) -- [ ] Bloque de identificación: `*IDN?` y detección automática de instrumento +### Comunicación con instrumentación (TCP/IP y USBTMC) +- [ ] TCP/IP cliente: bloques tcp-connect/write/read/close (#19) +- [ ] USBTMC: acceso a `/dev/usbtmc*` con misma interfaz (#20) +- [ ] Manejo de timeouts y errores de red/USB + +> **Nota:** NO se implementan bloques SCPI específicos. SCPI es un protocolo de comandos en texto que el usuario envía como string a través de `tcp-write`/`usbtmc-write`. Esto mantiene QTorres genérico y sirve también para Modbus (#22), protocolos propios y cualquier otro protocolo sobre TCP/USB. ### Comunicación serie - [ ] Puerto serie RS-232/RS-485: bloques open/write/read/close (#30) @@ -212,7 +213,7 @@ por equipos") implica que, una vez red-sg esté estable, QTorres delega en él l gráfica genérica (scene graph, transforms, hit-test, undo/redo, widgets). **Prerrequisitos:** -- Fase 4 funcionalmente completa (hardware operativo en al menos SCPI + Serial) +- Fase 4 funcionalmente completa (hardware operativo en al menos TCP/IP + Serial) - red-sg Fase 1 estable: sg-core, sg-transform, sg-hit-test, sg-events, sg-undo probados - Baselines de rendimiento establecidos (ver "Métricas pendientes" en roadmap-9-10) diff --git a/docs/review-issue-19.md b/docs/review-issue-19.md new file mode 100644 index 0000000..bc6b9f2 --- /dev/null +++ b/docs/review-issue-19.md @@ -0,0 +1,180 @@ +# Code Review — Issue #19: TCP/IP Bloques + +> Fecha: 2026-04-21 +> Estado: PENDING FIX +> Commits revisados: `2f55d89` (TCP v1), `1131006` (TCP v2 session-through), cambios working directory (v3 connection refnum) +> Tests: 558/558 PASS (sin errores, pero no cubren los P1 detectados) + +## Resumen + +Los cambios implementan 4 bloques TCP (`tcp-open`, `tcp-write`, `tcp-read`, `tcp-close`) con patrón connection refnum estilo LabVIEW, puertos cableables con defaults, y salidas `bytes-written`/`bytes-read`. El diseño es sólido y alineado con LabVIEW, pero hay hallazgos de seguridad y calidad que deben corregirse antes de merge. + +## Hallazgos + +| Priority | Issue | Location | +|----------|-------|----------| +| P1 | `_tcp-open-helper` ignora `timeout-ms` — no llama a `tcp/set-timeout` antes de `tcp/connect` | `src/graph/blocks.red:394` | +| P1 | Wire color usa tipo obsoleto `'tcp-session` en vez de `'tcp-connection` | `src/ui/diagram/canvas-render.red:172` | +| P1 | `_tcp-write-helper` no propaga error si `tcp/send` falla (devuelve `bytes` calculado antes del envío real) | `src/graph/blocks.red:399-404` | +| P1 | `_tcp-close-helper` marca `active? false` Y llama `tcp/close` — si `tcp/close` falla, la conexión queda en estado inconsistente (marcada cerrada pero real abierta) | `src/graph/blocks.red:418-422` | +| P2 | Helpers TCP se definen dos veces: en `blocks.red` (para tests) Y embebidos como strings en `file-io-serialize.red` — duplicación frágil | `src/graph/blocks.red:390-422`, `src/io/file-io-serialize.red:412-444` | +| P2 | `_tcp-read-helper` en `file-io-serialize.red:443` usa `{} ` (empty block) para string vacío, pero en `blocks.red:407` usa `""` — inconsistencia que puede causar bugs sutil en el `.qvi` generado | `src/io/file-io-serialize.red:443` | +| P2 | `bind-emit` cambio `append result item` → `append/only result item` en la rama `true` — este cambio afecta a TODOS los bloques, no solo TCP. Falta justificación y test de regresión específico | `src/compiler/compiler-emit.red:52` | +| P2 | `examples/test-tcp.qvi` es un fichero no trackeado que parece generado desde la UI con formato diferente al `tcp-echo-demo.qvi` — decidir si se mantiene o se excluye | `examples/test-tcp.qvi` (untracked) | +| P2 | Sin validación de puerto 0 o negativo en `tcp-open` — `tcp/connect` recibe `to-integer port` sin validación | `src/graph/blocks.red:395` | +| P3 | `tcp-open` default `"localhost"` es ambiguo (puede no resolver en algunos sistemas) — podría ser `"127.0.0.1"` para ser consistente con docs | `src/graph/blocks.red:426` | + +## Detalles de P1 + +### P1-1: `_tcp-open-helper` ignora `timeout-ms` + +**Fichero:** `src/graph/blocks.red:394-397` + +El helper acepta `timeout-ms` como parámetro pero no llama `tcp/set-timeout` antes de `tcp/connect`. Si la conexión remota no responde, el programa se bloquea indefinidamente. La API TCP (`docs/tcp-api.md`) indica que `tcp/set-timeout` aplica a `receive`, pero el `connect` en sí también debería tener timeout. + +```red +; FIX: llamar tcp/set-timeout antes de tcp/connect +_tcp-open-helper: func [host port timeout-ms /local ok] [ + tcp/set-timeout to-integer timeout-ms + ok: tcp/connect host to-integer port + _make-tcp-connection ok host to-integer port +] +``` + +> **Nota (Claude, 2026-04-21):** De acuerdo, bug real. Matiz: según la propia `docs/tcp-api.md`, `tcp/set-timeout` aplica a `receive`, no al `connect`. El fix sigue siendo correcto porque establece el timeout antes de la primera lectura — pero el `connect` en sí seguirá bloqueado por el timeout del SO. No es un fix "completo", es el mejor que Red 0.6.6 permite. **Aceptado, aplicar.** + +### P1-2: Wire color obsoleto `'tcp-session` vs `'tcp-connection` + +**Fichero:** `src/ui/diagram/canvas-render.red:172` + +Los bloques usan tipo `'tcp-connection` pero `wire-data-color` sigue comprobando `'tcp-session`. Esto significa que los wires de conexión TCP se renderizan con color naranja por defecto (`col-wire`) en vez de verde oliva (`col-wire-session`). Visualmente incorrecto. + +```red +; FIX: actualizar el tipo +data-type = 'tcp-connection [col-wire-session] +``` + +> **Nota (Claude, 2026-04-21):** Confirmado. Grep muestra `canvas-render.red:172` sigue con `'tcp-session` residual del v1. Fix de 1 línea, alto impacto visual. **Aceptado, aplicar.** + +### P1-3: `_tcp-write-helper` no detecta fallo de `tcp/send` + +**Fichero:** `src/graph/blocks.red:399-404` + +`tcp/send` devuelve `logic!` (true/false), pero el helper ignora el resultado. Además, `bytes` se calcula con `length? to-binary data` ANTES del envío — si `tcp/send` falla, `bytes-written` refleja la longitud del buffer, no los bytes realmente enviados. El programa continua como si el envío hubiera tenido éxito. + +```red +; FIX: usar el retorno de tcp/send +_tcp-write-helper: func [conn data /local sent bytes] [ + if not conn/active? [return reduce [conn 0]] + bytes: length? to-binary data + sent: tcp/send data + either sent [reduce [conn bytes]] [reduce [conn 0]] +] +``` + +> **Nota (Claude, 2026-04-21):** De acuerdo. Dos líneas, y `bytes-written=0` tras fallo es mucho más honesto. **Aceptado, aplicar.** + +### P1-4: Estado inconsistente en `_tcp-close-helper` + +**Fichero:** `src/graph/blocks.red:418-422` + +Si `tcp/close` falla (error de red, conexión reseteada por el peer), la conexión TCP real sigue abierta pero el objeto se marca `active?: false`. En posteriores operaciones (con error cluster en Fase 4-E), no se puede intentar cerrarla de nuevo ni manejar el error. + +```red +; FIX: solo marcar inactiva si tcp/close tiene éxito +_tcp-close-helper: func [conn /local ok] [ + if not conn/active? [return conn] + ok: tcp/close + either ok [ + _make-tcp-connection false conn/host conn/port + ][ + conn ; mantener active? true si tcp/close falla + ] +] +``` + +> **Nota (Claude, 2026-04-21):** Matiz. Bajo DT-029 Nivel 0 (error handling actual) el programa se para con error nativo si `tcp/close` falla — no hay ruta de recuperación posible sin error cluster. El fix propuesto sólo tiene sentido cuando exista Nivel 2 (#19-b o Fase 4-E). **Diferir a issue de error cluster; no bloqueante.** + +## Detalles de P2 + +### P2-1: Duplicación frágil de helpers + +Los 5 helpers TCP están duplicados: como funciones Red en `blocks.red` (accesibles en tests) y como strings literales en `file-io-serialize.red` (inyectados en el `.qvi`). Cualquier cambio en uno debe replicarse manualmente en el otro. Las versiones YA están desincronizadas (ver P2-2). + +Posible solución: extraer los helpers a una fuente única que `file-io-serialize.red` lea con `mold` en vez de strings literales, o generar un test que verifique consistencia. + +> **Nota (Claude, 2026-04-21):** De acuerdo, coincide con mi audit. La solución vía `mold` sobre las funciones reales es el camino correcto: única fuente de verdad, test automático implícito. **Diferir a issue de refactor pero crear ticket ahora para no olvidarlo.** + +### P2-2: Inconsistencia `""` vs `{}` en helpers serializados + +En `blocks.red:407`, el helper `_tcp-read-helper` devuelve `""` (string vacío) como no-op. En `file-io-serialize.red:443`, la versión serializada usa `{} ` (empty block). En Red, `form {}` produce `"{}"` (literalmente los caracteres llaves), no un string vacío. Esto cambia el comportamiento del `.qvi` generado vs el código en `blocks.red`. + +Solución: unificar a `""` en ambos sitios. + +> **Nota (Claude, 2026-04-21) — FALSO POSITIVO:** En Red, `{...}` es sintaxis **alternativa de string literal** (usada para multilínea), NO sintaxis de bloque. El bloque vacío es `[]`, no `{}`. Verificado empíricamente con `./red-cli`: +> +> ``` +> equal? {} "" → true +> type? {} → string! +> length? {} → 0 +> ``` +> +> Por tanto `reduce [conn {} 0]` y `reduce [conn "" 0]` son idénticos. No hay divergencia de comportamiento ni bug. `form {}` produce `""` (string vacío), no `"{}"`. **Rechazado, no aplicar.** (Conviene unificar por estilo, pero no es un fix.) + +### P2-3: Cambio `append/only` en `bind-emit` + +El cambio de `append result item` a `append/only result item` en la rama `true` de `bind-emit` afecta el comportamiento para TODOS los bloques, no solo TCP. `append/only` envuelve valores block! en un bloque adicional. Necesita test de regresión explícito o justificación documentada de por qué es seguro para bloques existentes. + +> **Nota (Claude, 2026-04-21):** Matiz técnico. La rama `true` sólo se alcanza con items que no son `word!`, `set-word!` ni `block!`. En la práctica: `path!`, `lit-word!`, literales (integer!, float!, string!, logic!). Para escalares, `append` y `append/only` son idénticos. La diferencia solo importa para `path!` (antes se aplanaba: `_w/1` → `_w 1`, lo que rompía el bind). El fix es **correcto y necesario** — sin él, los nuevos bloques TCP emiten código roto. Los 558 tests PASS tras el cambio validan que no hay regresión en bloques existentes. **Aceptado; añadir test de regresión específico (`path!` en emit preserva estructura).** + +### P2-4: Fichero `examples/test-tcp.qvi` sin trackear + +Fichero generado desde la UI con formato diferente (nodos en múltiples líneas, nombres de puerto con guión en wires, título con path absoluto). Parece un artefacto de prueba. Decidir si se excluye (`.gitignore`) o se borra. + +> **Nota (Claude, 2026-04-21):** Ya resuelto — movido de `src/` a `examples/test-tcp.qvi` y `src/untitled.qvi` borrado. El fichero se conserva como prueba adicional generada por la UI (complementa al `tcp-echo-demo.qvi` manual). Diferencias de formato son naturales entre escritura manual y el serializador. **Resuelto.** + +### P2-5: Sin validación de puerto + +`tcp-open` usa `to-integer port` sin validar que el puerto esté en rango 1-65535. Puerto 0 o negativo provocaría comportamiento indefinido en `tcp/connect`. + +> **Nota (Claude, 2026-04-21):** Válido pero bajo DT-029 Nivel 0 la política es dejar que Red propague el error nativo. Añadir validación aquí rompe la consistencia con el resto de bloques (ningún otro valida rangos). **Diferir a issue de Fase 4-E (error cluster)** — ahí cobra sentido tener `error-out` en lugar de crash silencioso. + +## Detalle de P3 + +### P3-1: Default `"localhost"` en `tcp-open` + +El default `"localhost"` puede no resolver en sistemas sin `/etc/hosts` configurado o en contenedores Docker. Considerar cambiar a `"127.0.0.1"` para consistencia con la documentación (`docs/tcp-api.md`) y el ejemplo `tcp-echo-demo.qvi`. + +> **Nota (Claude, 2026-04-21):** Preferencia, no bug. `localhost` resuelve por defecto en Linux/macOS/Windows estándar. Docker sin `/etc/hosts` no es escenario realista para QTorres (desktop-oriented). Dejarlo como `"localhost"` es más legible para el usuario. **Rechazado.** + +## Acciones pendientes + +- [ ] Fix P1-1: timeout en `_tcp-open-helper` +- [ ] Fix P1-2: wire color `'tcp-connection` +- [ ] Fix P1-3: error handling en `_tcp-write-helper` +- [ ] Fix P1-4: estado consistente en `_tcp-close-helper` +- [ ] Fix P2-2: unificar `""` vs `{}` en helpers serializados +- [ ] Evaluar P2-1, P2-3, P2-4, P2-5 +- [ ] Re-ejecutar tests tras fixes + +--- + +## Triage final (Claude, 2026-04-21) + +| Hallazgo | Decisión | Motivo | +|----------|----------|--------| +| P1-1 timeout | ✅ Aplicar | Bug real, parámetro decorativo sin el fix | +| P1-2 wire color | ✅ Aplicar | Bug visual, fix de 1 línea | +| P1-3 `tcp/send` check | ✅ Aplicar | Honestidad en `bytes-written` | +| P1-4 close state | ⏸ Diferir | Requiere error cluster (Fase 4-E) | +| P2-1 duplicación helpers | 📋 Ticket | Refactor vía `mold` post-commit | +| P2-2 `""` vs `{}` | ❌ Rechazar | Falso positivo, son equivalentes | +| P2-3 append/only | ✅ Aplicar | Añadir test de regresión | +| P2-4 test-tcp.qvi | ✅ Resuelto | Movido a `examples/` | +| P2-5 validación puerto | ⏸ Diferir | Cohabita con error cluster | +| P3-1 localhost | ❌ Rechazar | Preferencia, funciona en sistemas normales | + +**Bloqueantes antes del commit:** P1-1, P1-2, P1-3, P2-3 (con su test). +**Diferir a ticket de Fase 4-E:** P1-4, P2-1, P2-5. + +**Nota global al reviewer:** revisión sólida (8/10 hallazgos válidos), falso positivo en P2-2 revela desconocimiento de la sintaxis `{...}` como string literal alternativo en Red. Recomendado añadir al skill `red-lang/SKILL.md` una nota sobre esa equivalencia para evitar repetirlo. \ No newline at end of file diff --git a/docs/roadmap-9-10.md b/docs/roadmap-9-10.md index 6029ebc..3a22add 100644 --- a/docs/roadmap-9-10.md +++ b/docs/roadmap-9-10.md @@ -368,7 +368,7 @@ mínimos reproducibles: --- -### Fase 4 — Hardware (SCPI, Serial, TCP/IP, DAQ) +### Fase 4 — Hardware (TCP/IP, USBTMC, Serial, DAQ) > **Prioridad estratégica:** Fase 4 va **antes** que Fase 5 (UX). Un QTorres que habla con > instrumentos reales aporta valor a ingenieros de laboratorio; un QTorres con undo/redo @@ -405,14 +405,17 @@ concurrencia cooperativa (DT-027): ```red ; Patrón: intentar operación con timeout -scpi-query: func [instrument command /timeout ms /local result timer] [ +tcp-query: func [command /timeout ms /local result timer] [ timer: make object! [expired: false] - ; ... implementar con rate/on-time o callback + tcp/set-timeout ms + tcp/send command + tcp/receive 1024 + ; ... implementar polling con rate/on-time si se requiere no-bloqueante ] ``` -**Nota:** Esto es investigación y diseño, no solo implementación. La estrategia exacta -depende de las capacidades de Red para I/O con timeout, que deben validarse primero. +**Nota:** La API TCP nativa (`tcp/set-timeout`, `tcp/readable?`) ya da soporte básico. +Para no-bloqueante real con integración GUI hay que combinar con `face/rate`+`on-time`. #### 4.3 Tests de compilación con `red -c` (PRIORIDAD MEDIA) @@ -858,8 +861,8 @@ una verdad revelada, se listan aquí sus puntos débiles conocidos: 3. **Priorización ALTA/MEDIA/BAJA** — sigue siendo subjetiva. Menos deshonesta que la puntuación decimal original, pero no es objetiva. 4. **"Fase 4 antes que Fase 5"** — es la opinión actual del autor del roadmap. Un - usuario real podría considerar undo/redo más urgente que SCPI si su caso de uso - no incluye hardware. + usuario real podría considerar undo/redo más urgente que hardware si su caso de uso + no incluye instrumentación. ### Decisiones que este documento no justifica diff --git a/docs/tcp-api.md b/docs/tcp-api.md new file mode 100644 index 0000000..67ae338 --- /dev/null +++ b/docs/tcp-api.md @@ -0,0 +1,199 @@ +# TCP API — Referencia de Red + +> Integrado en binarios `red-cli` y `red-view` desde fork `anlaco/red` (Fase 4+) + +## API de bajo nivel + +El objeto `tcp` expone una API nativa de sockets TCP para comunicación red. + +### Funciones principales + +#### `tcp/connect host port → logic!` + +Conecta a un servidor TCP. + +```red +if tcp/connect "192.168.1.100" 5025 [ + print "Conectado a instrumento" +] [ + print "Error de conexión" +] +``` + +- **host** `[string!]` — dirección IP o nombre de host +- **port** `[integer!]` — puerto TCP (1-65535) +- **return** `[logic!]` — true si éxito, false si fallo + +#### `tcp/send data → logic!` + +Envía datos al servidor. + +```red +tcp/send "GET / HTTP/1.0^/^/" ; texto +tcp/send to-binary! "datos" ; bytes +``` + +- **data** `[string! binary!]` — datos a enviar +- **return** `[logic!]` — true si enviado, false si error + +#### `tcp/receive size → binary! | none!` + +Recibe datos del servidor (bloqueante). + +```red +response: tcp/receive 1024 +if response [ + print to string! response +] +``` + +- **size** `[integer!]` — máximo bytes a recibir +- **return** `[binary! none!]` — datos recibidos o none si error/desconectado + +#### `tcp/close → logic!` + +Cierra la conexión. + +```red +tcp/close +``` + +- **return** `[logic!]` — true siempre + +### Estado y opciones + +#### `tcp/connected? → logic!` + +Estado actual de la conexión. + +```red +if tcp/connected? [ + print "Conectado" +] +``` + +#### `tcp/set-timeout ms → logic!` + +Timeout para receive (milisegundos). + +```red +tcp/set-timeout 5000 ; esperar max 5 segundos +data: tcp/receive 256 +``` + +#### `tcp/readable? → logic!` + +Verifica si hay datos disponibles sin bloquear. + +```red +if tcp/readable? [ + data: tcp/receive 256 +] +``` + +#### `tcp/receive-available size → binary! | none!` + +Recibe datos disponibles sin bloquear. + +```red +data: tcp/receive-available 256 ; no bloquea +``` + +#### `tcp/set-nonblocking enable → logic!` + +Modo no-bloqueante (avanzado). + +```red +tcp/set-nonblocking true +``` + +#### `tcp/last-error → object!` + +Obtiene último error. + +```red +error: tcp/last-error +print error/message +``` + +## Casos de uso — Fase 4 + +### Eco genérico (cliente TCP) + +```red +Red [Needs: 'View] + +; Conectar a servidor +if not tcp/connect "192.168.1.100" 5000 [ + print "Error: no se pudo conectar" + halt +] + +; Enviar petición +tcp/send "PING^/" + +; Leer respuesta +response: tcp/receive 256 +print ["Respuesta: " to string! response] + +; Cerrar +tcp/close +``` + +> Para enviar comandos de instrumentación (texto plano como `*IDN?`, `MEAS:VOLT?`, +> cadenas Modbus, etc.) basta con poner el string adecuado en `tcp/send`. QTorres no +> incluye bloques específicos por protocolo — el usuario elige qué cadena enviar. + +### Lectura secuencial (con timeout) + +```red +tcp/set-timeout 2000 + +loop 10 [ + data: tcp/receive 64 + if data [ + print ["Dato " index ": " to string! data] + ] +] + +tcp/close +``` + +### Polling no-bloqueante + +```red +tcp/set-nonblocking true + +loop 100 [ + if tcp/readable? [ + data: tcp/receive 256 + process-data data + ] + wait 0.01 ; evitar busy-loop +] + +tcp/close +``` + +## Notas de implementación + +- **Bloqueante por defecto:** `tcp/receive` bloquea hasta recibir datos o timeout +- **Terminación de línea:** muchos protocolos de texto requieren `\n` o `\r\n` al final de cada mensaje — usar `rejoin [cmd newline]` +- **Binary vs String:** TCP transporta bytes. Convertir con `to string!` / `to binary!` cuando el protocolo sea texto +- **Sin hilos:** Red no tiene multihilo. Para múltiples conexiones, usar polling no-bloqueante + `on-time` / timers (DT-027) +- **Error handling:** revisar `tcp/last-error` si `connect` o `send` fallan + +## Integración QTorres (Fase 4) + +Los bloques de hardware (#19, #22) usarán esta API de forma genérica: + +- **tcp-connect / tcp-write / tcp-read / tcp-close** → wrappers directos de `tcp/connect`, `tcp/send`, `tcp/receive`, `tcp/close` +- **Error cluster** → `tcp/last-error` mapea a puertos error-in/error-out +- **Timeout configurable** → parámetro de bloque → `tcp/set-timeout` +- **Modbus TCP** (#22) → syntactic sugar que construye la trama Modbus y la envía con `tcp/send` + +> QTorres no incluye bloques específicos por protocolo (HTTP, SCPI, MQTT, …). Cada +> protocolo de texto se usa pasando la cadena adecuada al bloque `tcp-write`. +> Protocolos binarios (Modbus, custom) pueden construirse con `to-binary!`. + +Ver `docs/plan.md` — Fase 4 para roadmap completo. diff --git a/examples/tcp-echo-demo.qvi b/examples/tcp-echo-demo.qvi new file mode 100644 index 0000000..28b2264 --- /dev/null +++ b/examples/tcp-echo-demo.qvi @@ -0,0 +1,93 @@ +Red [Title: "tcp-echo-demo" Needs: 'View] + +; Demo TCP/IP — estilo LabVIEW (connection refnum, puertos cableables). +; +; Requiere servidor de eco en 127.0.0.1:5000. +; Para probar: socat TCP-LISTEN:5000,reuseaddr,fork EXEC:/bin/cat +; +; Cableado: +; str-const("127.0.0.1") → tcp-open/address +; num-const(5000) → tcp-open/remote-port +; timeout-ms sin cablear → usa default 60000 +; str-control(Mensaje) → tcp-write/data +; num-const(256) → tcp-read/bytes-to-read +; tcp-read/data → str-indicator(Respuesta) + +qvi-diagram: [ + meta: [description: "Cliente TCP eco — estilo LabVIEW" version: 3 author: "" tags: [tcp hardware demo]] + icon: [] + front-panel: [ + control [id: 1 type: 'string name: "msg_1" label: [text: "Mensaje"] default: "HELLO"] + indicator [id: 2 type: 'string name: "resp_1" label: [text: "Respuesta"]] + ] + block-diagram: [ + nodes: [ + node [id: 1 type: 'str-control x: 40 y: 200 name: "msg_1" label: [text: "Mensaje" visible: true]] + node [id: 2 type: 'str-const x: 40 y: 40 name: "str-const_1" label: [text: "str-const" visible: false] config: [default "127.0.0.1"]] + node [id: 3 type: 'const x: 40 y: 120 name: "const_1" label: [text: "const" visible: false] config: [default 5000]] + node [id: 4 type: 'tcp-open x: 220 y: 60 name: "tcp-open_1" label: [text: "tcp-open" visible: true]] + node [id: 5 type: 'tcp-write x: 400 y: 120 name: "tcp-write_1" label: [text: "tcp-write" visible: true]] + node [id: 6 type: 'tcp-read x: 580 y: 120 name: "tcp-read_1" label: [text: "tcp-read" visible: true]] + node [id: 7 type: 'tcp-close x: 760 y: 160 name: "tcp-close_1" label: [text: "tcp-close" visible: true]] + node [id: 8 type: 'const x: 580 y: 40 name: "const_2" label: [text: "const" visible: false] config: [default 256]] + node [id: 9 type: 'str-indicator x: 580 y: 280 name: "resp_1" label: [text: "Respuesta" visible: true]] + ] + wires: [ + ; constantes → tcp-open + wire [from: 2 port: 'result to: 4 port: 'address] + wire [from: 3 port: 'result to: 4 port: 'remote-port] + ; connection refnum encadenado + wire [from: 4 port: 'connection-out to: 5 port: 'connection-in] + wire [from: 5 port: 'connection-out to: 6 port: 'connection-in] + wire [from: 6 port: 'connection-out to: 7 port: 'connection-in] + ; datos + wire [from: 1 port: 'result to: 5 port: 'data] + wire [from: 8 port: 'result to: 6 port: 'bytes-to-read] + wire [from: 6 port: 'data to: 9 port: 'in] + ] + ] +] + +; --- Helpers de runtime --- +_make-tcp-connection: func [a? h p] [make object! [active?: a? host: h port: p]] +_tcp-open-helper: func [host port timeout-ms /local ok] [ok: tcp/connect host to-integer port _make-tcp-connection ok host to-integer port] +_tcp-write-helper: func [conn data /local bytes] [if not conn/active? [return reduce [conn 0]] bytes: length? to-binary data tcp/send data reduce [conn bytes]] +_tcp-read-helper: func [conn sz timeout-ms /local buf bytes] [if not conn/active? [return reduce [conn "" 0]] tcp/set-timeout to-integer timeout-ms buf: tcp/receive to-integer sz either buf [bytes: length? buf reduce [conn to string! buf bytes]] [reduce [conn "" 0]]] +_tcp-close-helper: func [conn] [if not conn/active? [return conn] tcp/close _make-tcp-connection false conn/host conn/port] + +; --- CÓDIGO GENERADO — no editar, se regenera al guardar --- +either empty? system/options/args [ + view layout [ + text "Mensaje:" f_msg_1: field "HELLO" 200x30 return + text "Respuesta:" f_resp_1: field "" 200x30 return + button "Run" [ + msg_1: f_msg_1/text + str-const_1: "127.0.0.1" + const_1: 5000 + tcp-open_1: _tcp-open-helper str-const_1 const_1 60000 + _w: _tcp-write-helper tcp-open_1 msg_1 + tcp-write_1: _w/1 + tcp-write_1_bytes: _w/2 + const_2: 256 + _r: _tcp-read-helper tcp-write_1 const_2 60000 + tcp-read_1: _r/1 + resp_1: _r/2 + tcp-close_1: _tcp-close-helper tcp-read_1 + f_resp_1/text: resp_1 + ] + ] +][ + msg_1: system/options/args/1 + str-const_1: "127.0.0.1" + const_1: 5000 + tcp-open_1: _tcp-open-helper str-const_1 const_1 60000 + _w: _tcp-write-helper tcp-open_1 msg_1 + tcp-write_1: _w/1 + tcp-write_1_bytes: _w/2 + const_2: 256 + _r: _tcp-read-helper tcp-write_1 const_2 60000 + tcp-read_1: _r/1 + resp_1: _r/2 + tcp-close_1: _tcp-close-helper tcp-read_1 + print resp_1 +] diff --git a/examples/test-tcp.qvi b/examples/test-tcp.qvi new file mode 100644 index 0000000..33946e0 --- /dev/null +++ b/examples/test-tcp.qvi @@ -0,0 +1,128 @@ +Red [Title: {/home/alaforga/Anlaco/01-PRODUCTOS/QTorres/src/test-tcp.qvi} Needs: 'View] + +qvi-diagram: [ + meta: [description: "" version: 1 author: "" tags: []] + icon: [] + block-diagram: [ + nodes: [ + node [ + id: 1 type: str-control + name: "str-control_1" + label: [text: "" visible: false] + x: 325.0 y: 260.0 config [default "HOLA"] +] + node [ + id: 2 type: str-indicator + name: "str-indicator_2" + label: [text: "" visible: false] + x: 1077.0 y: 289.0 +] + node [ + id: 3 type: str-const + name: "str-const_1" + label: [text: "" visible: false] + x: 70.0 y: 153.0 config [default "127.0.0.1"] +] + node [ + id: 4 type: const + name: "const_1" + label: [text: "" visible: false] + x: 61.0 y: 220.0 config [default 5000.0] +] + node [ + id: 5 type: tcp-open + name: "tcp-open_1" + label: [text: "" visible: false] + x: 303.0 y: 189.0 +] + node [ + id: 6 type: tcp-write + name: "tcp-write_1" + label: [text: "" visible: false] + x: 564.0 y: 189.0 +] + node [ + id: 7 type: const + name: "const_2" + label: [text: "" visible: false] + x: 595.0 y: 273.0 config [default 256.0] +] + node [ + id: 8 type: tcp-read + name: "tcp-read_1" + label: [text: "" visible: false] + x: 839.0 y: 189.0 +] + node [ + id: 9 type: tcp-close + name: "tcp-close_1" + label: [text: "" visible: false] + x: 1077.0 y: 190.0 +] + ] + wires: [ + wire [ + from: 3 from-port: result + to: 5 to-port: address +] + wire [ + from: 4 from-port: result + to: 5 to-port: remote-port +] + wire [ + from: 5 from-port: connection-out + to: 6 to-port: connection-in +] + wire [ + from: 1 from-port: result + to: 6 to-port: data +] + wire [ + from: 6 from-port: connection-out + to: 8 to-port: connection-in +] + wire [ + from: 7 from-port: result + to: 8 to-port: bytes-to-read +] + wire [ + from: 8 from-port: connection-out + to: 9 to-port: connection-in +] + wire [ + from: 8 from-port: data + to: 2 to-port: value +] + ] + ] + front-panel: [ + str-control [id: 1 type: str-control name: "str-control_1" label: [text: "String" visible: true offset: 0x0] default: "HOLA" offset: 119x83] + str-indicator [id: 2 type: str-indicator name: "str-indicator_2" label: [text: "String" visible: true offset: 0x0] default: "HOLA" offset: 447x91] + ] +] + +; --- Helpers de runtime --- +arr-subset-helper: func [arr st ln] [copy/part skip arr to-integer st to-integer ln] +_make-tcp-connection: func [a? h p] [make object! [active?: a? host: h port: p]] +_tcp-open-helper: func [host port timeout-ms /local ok] [ok: tcp/connect host to-integer port _make-tcp-connection ok host to-integer port] +_tcp-write-helper: func [conn data /local bytes] [if not conn/active? [return reduce [conn 0]] bytes: length? to-binary data tcp/send data reduce [conn bytes]] +_tcp-read-helper: func [conn sz timeout-ms /local buf bytes] [if not conn/active? [return reduce [conn {} 0]] tcp/set-timeout to-integer timeout-ms buf: tcp/receive to-integer sz either buf [bytes: length? buf reduce [conn to string! buf bytes]] [reduce [conn {} 0]]] +_tcp-close-helper: func [conn] [if not conn/active? [return conn] tcp/close _make-tcp-connection false conn/host conn/port] + +; --- CÓDIGO GENERADO — no editar, se regenera al guardar --- +either empty? system/options/args [ + view layout [ + text "" f_1: field "HOLA" + text "" f_3: field "127.0.0.1" + text "" f_4: field "5000.0" + text "" f_7: field "256.0" + button "Run" [str-control_1_result: f_1/text str-const_1_result: f_3/text const_1_result: to-float f_4/text const_2_result: to-float f_7/text tcp-open_1_connection-out: _tcp-open-helper str-const_1_result const_1_result 60000 +_w: _tcp-write-helper tcp-open_1_connection-out str-control_1_result tcp-write_1_connection-out: _w/1 tcp-write_1_bytes-written: _w/2 +_r: _tcp-read-helper tcp-write_1_connection-out const_2_result 60000 tcp-read_1_connection-out: _r/1 tcp-read_1_data: _r/2 tcp-read_1_bytes-read: _r/3 tcp-close_1_connection-out: _tcp-close-helper tcp-read_1_connection-out t_2/text: form tcp-read_1_data] + text "" t_2: text "---" + ] +][ + str-control_1_result: "HOLA" str-const_1_result: "127.0.0.1" const_1_result: 5000.0 const_2_result: 256.0 tcp-open_1_connection-out: _tcp-open-helper str-const_1_result const_1_result 60000 +_w: _tcp-write-helper tcp-open_1_connection-out str-control_1_result tcp-write_1_connection-out: _w/1 tcp-write_1_bytes-written: _w/2 +_r: _tcp-read-helper tcp-write_1_connection-out const_2_result 60000 tcp-read_1_connection-out: _r/1 tcp-read_1_data: _r/2 tcp-read_1_bytes-read: _r/3 tcp-close_1_connection-out: _tcp-close-helper tcp-read_1_connection-out print rejoin ["" ": " form tcp-read_1_data] +] diff --git a/red-cli b/red-cli index 83aa9a4..47ca36c 100755 Binary files a/red-cli and b/red-cli differ diff --git a/red-view b/red-view index bcc34e2..95e04a6 100755 Binary files a/red-view and b/red-view differ diff --git a/skills/red-lang/SKILL.md b/skills/red-lang/SKILL.md new file mode 100644 index 0000000..c138222 --- /dev/null +++ b/skills/red-lang/SKILL.md @@ -0,0 +1,508 @@ +# Red-Lang Skill para QTorres + +> Referencia rápida de Red para codificar QTorres. Consultar antes de escribir código Red, especialmente Draw y View. + +**Repositorio Red oficial:** https://www.red-lang.org +**Documentación:** https://doc.red-lang.org +**Versión:** 0.6.6+ +**QTorres:** Red 100% (DT-001) + +--- + +## Sintaxis core + +### Tipos de datos básicos + +| Tipo | Ejemplos | Notas | +|------|----------|-------| +| `integer!` | `5` `-42` `0` | Números enteros | +| `float!` | `3.14` `1e-3` | Punto flotante | +| `string!` | `"hola"` `{multi\nlínea}` | Texto | +| `binary!` | `#{48656C6C6F}` | Bytes | +| `logic!` | `true` `false` | Booleano | +| `block!` | `[1 2 3]` `[a: 5 print a]` | Bloque (código/datos) | +| `object!` | `make object! [a: 1 b: 2]` | Diccionario con propiedades | +| `function!` | `func [x][x * 2]` | Función con contexto | +| `routine!` | Rutinas nativas C | (Compiladas, no escribir en Red) | + +### Variables y contextos + +```red +; Asignar +a: 10 +x: "texto" + +; Objeto (diccionario) +person: make object! [ + name: "Alice" + age: 30 + greet: func [][print ["Hola, " name]] +] +person/name ; acceso +person/greet ; llamar método + +; Contexto (namespace) +make-context: function [][ + value: 42 + func [][value] +] +``` + +### Control de flujo + +```red +; if +if condition [ + ; true +] [ + ; false (opcional) +] + +; either (if/else) +either x > 5 [print "grande"] [print "pequeño"] + +; loop +loop 10 [print "x"] + +; foreach +foreach [k v] [a 1 b 2 c 3] [print [k v]] + +; while +while [condition] [; body] + +; until +until [condition] + +; try/catch +try [dangerous-code] [error-code] +``` + +### Funciones + +```red +; función simple +add: func [a b][a + b] + +; con tipo de retorno +increment: func [ + n [integer!] + return: [integer!] +][ + n + 1 +] + +; función local (sin contaminar contexto global) +local-func: function [x][ ; function = func + contexto nuevo + local-var: x * 2 + local-var +] +``` + +### Manejo de bloques + +```red +; map-like (procesar bloque) +process: func [blk][ + result: [] + foreach item blk [ + append result item * 2 + ] + result +] + +; compose (interpolar) +name: "Alice" +msg: compose ["Hola, " (name)] ; ["Hola, " "Alice"] + +; parse (parser simple) +rule: [integer! string! integer!] +parse [5 "hola" 10] rule ; true si encaja +``` + +--- + +## View — UI Imperatva + +### Layout básico + +```red +view layout [ + text "Etiqueta" + input: field 200x30 + button "Aceptar" [ + print input/text + ] +] +``` + +### Faces (widgets) + +| Face | Uso | +|------|-----| +| `text` | Etiqueta estática | +| `field` | Input texto | +| `button` | Botón clickable | +| `check` | Checkbox | +| `radio` | Radio button | +| `slider` | Slider (0-100) | +| `progress` | Barra de progreso | +| `panel` | Contenedor | +| `base` | Canvas base (para Draw) | +| `label` | Etiqueta editable | + +### Propiedades comunes + +```red +face: field 200x30 +face/text: "nuevo valor" +face/visible: true +face/enabled: true +face/offset: 10x20 ; posición (x, y) +face/size: 200x30 ; ancho x alto +face/color: 128.128.128 ; RGB tuple +face/font: make font! [size: 12] +``` + +### Events (callbacks) + +```red +button "Click" [ + ; this = la face que gatilló el evento + print ["Botón presionado: " this/text] +] + +; on-time (timer) +view layout [ + base 300x200 [ + ; ejecutar cada face/rate ticks + face/rate: 10 ; 10 ticks/segundo + ] on-time [ + ; lógica del timer + ] +] +``` + +### Diálogos + +```red +; Open file +file: request-file/title "Selecciona fichero" + +; Color picker +color: request-color + +; Simple dialog +result: view/new compose [ + text (msg) + button "OK" [result: true] +] +``` + +--- + +## Draw — Gráficos vectoriales + +### Semántica + +```red +; Draw es un dialecto que genera vectores +; Se usa en `base/draw` o parámetro `draw:` de face + +draw-cmds: [ + line 10x10 100x100 + box 50x50 150x150 + circle 75x75 30 + text 10x10 "Hola" +] + +view layout [ + base 300x300 draw-cmds +] +``` + +### Comandos principales + +| Comando | Uso | +|---------|-----| +| `line pt1 pt2` | Línea | +| `box pt1 pt2` | Rectángulo relleno | +| `box pt1 pt2 /stroke` | Rectángulo solo borde | +| `circle center radius` | Círculo | +| `circle center radiusX radiusY` | Elipse | +| `polygon [pt1 pt2 pt3...]` | Polígono | +| `text offset string` | Texto | +| `image image-data offset` | Imagen | +| `fill-pen color` | Color de relleno | +| `pen color` | Color de borde | +| `line-width n` | Grosor línea | +| `font font-obj` | Fuente para text | + +### Colores + +```red +pen 255.0.0 ; RGB rojo +pen 128.128.128 ; Gris +pen red ; Nombre predefinido +pen #FF0000 ; Hex (con #) + +; Transparencia (si soportado) +pen rgba(255 0 0 128) ; 50% transparente +``` + +### Ejemplo completo + +```red +view layout [ + base 300x300 [ + pen 0.0.0 ; negro + line-width 2 + + fill-pen 200.200.255 + box 50x50 150x150 + + fill-pen 255.0.0 + circle 75x75 30 + + font make font! [size: 14] + text 10x10 "Ejemplo Draw" + ] +] +``` + +--- + +## Parse — Parsing de bloques + +> Dialecto de Red para reconocimiento de patrones. + +### Sintaxis básica + +```red +; Estructura: parse + +; Regla simple: encaja tipos +parse [1 "hola" 2] [integer! string! integer!] ; true + +; Captura en variable +data: [] +parse [1 2 3] [ + set x integer! (append data x) + set y integer! (append data y) + set z integer! +] +; data = [1 2] + +; Repetición +parse [1 1 1 2] [ + some integer! ; 1+ números + integer! ; último número +] + +; Alternación +parse "abc" [ + some ["a" | "b" | "c"] +] + +; Opcional +parse [1 2] [ + integer! opt integer! opt integer! +] +``` + +### Reglas de parse + +| Regla | Encaja | +|-------|--------| +| `integer!` | Cualquier entero | +| `"x"` / `'x` | Literal "x" | +| `[a b c]` | Secuencia | +| `[a \| b \| c]` | Alternativa | +| `some rule` | 1+ | +| `any rule` | 0+ | +| `opt rule` | 0-1 | +| `set var rule` | Captura | +| `copy var rule` | Copia bloque | + +### Ejemplo: parser de comandos + +```red +parse-command: func [cmd-string][ + result: make object! [action: none args: []] + + parse cmd-string [ + set result/action word! + any [set arg word! (append result/args arg)] + ] + + result +] + +cmd: parse-command "show point 10 20" +; cmd/action = 'show +; cmd/args = [point 10 20] +``` + +--- + +## TCP — Comunicación de red + +> API integrada en binarios red-cli/red-view (Fase 4+) + +### API rápida + +```red +; Conectar +if tcp/connect "192.168.1.100" 5000 [ + ; Enviar petición + tcp/send "PING^/" + + ; Recibir respuesta + response: tcp/receive 256 + + ; Procesar + print to string! response + + ; Cerrar + tcp/close +] +``` + +### Funciones + +| Función | Parámetros | Return | +|---------|-----------|--------| +| `tcp/connect` | `host [string!] port [integer!]` | `[logic!]` | +| `tcp/send` | `data [string! binary!]` | `[logic!]` | +| `tcp/receive` | `size [integer!]` | `[binary! none!]` | +| `tcp/close` | — | `[logic!]` | +| `tcp/connected?` | — | `[logic!]` | +| `tcp/set-timeout` | `ms [integer!]` | `[logic!]` | +| `tcp/readable?` | — | `[logic!]` | +| `tcp/set-nonblocking` | `enable [logic!]` | `[logic!]` | +| `tcp/last-error` | — | `[object!]` | + +### Ejemplo con timeout + +```red +Red [Needs: 'View] + +; Conectar a servidor +if not tcp/connect "192.168.1.100" 5000 [ + print "Error: conexión fallida" + halt +] + +; Enviar petición y leer con timeout +tcp/set-timeout 2000 +tcp/send "HELLO^/" + +response: tcp/receive 256 +if response [ + print ["Respuesta: " to string! response] +] + +tcp/close +``` + +> QTorres no incluye bloques específicos por protocolo. Para enviar comandos de texto +> de instrumentación, Modbus, HTTP, MQTT o similar, basta con poner el string adecuado +> en `tcp/send`. Ver `docs/tcp-api.md` para referencia completa. + +--- + +## Dialects propios de QTorres + +### block-def — Definición de bloques + +```red +; En src/graph/blocks.red + +register-block [ + type: 'add + label: "Add" + category: 'math + inputs: [a b] + outputs: [out] + emit: [out: a + b] +] +``` + +### qvi-diagram — Estructura de VI + +```red +qvi-diagram: [ + meta: [description: "..." version: 1] + connector: [...] ; opcional + front-panel: [ + control [id: 1 type: 'numeric name: "ctrl_1" label: [text: "A"]] + ] + block-diagram: [ + nodes: [...] + wires: [...] + ] +] +``` + +### emit — Generación de código Red + +```red +; Cómo un bloque genera Red al compilar +emit: [ + ; Cuerpo Red que se inserta en el VI compilado + out: input-a + input-b +] +``` + +--- + +## Gotchas y convenciones + +### No usar + +- ❌ `do` con bloques dinámicos en `.qvi` generado (debe compilarse con `red -c`) +- ❌ `load` de strings → use parse +- ❌ `compose` en runtime del VI generado (OK en compilador de QTorres) +- ❌ Herencia profunda (A → B → C) → usar composición +- ❌ Faces nativas en canvas del editor (usar Draw) +- ❌ Strings intermedios en compilador (manipular bloques Red) + +### Usar + +- ✅ `make object! [...]` para prototipos y composición +- ✅ `func` para funciones con cierre léxico +- ✅ `function` para contexto limpio (aislado) +- ✅ `view layout [...]` estático en VIs generados +- ✅ `face/rate` + `on-time` para timers/loops +- ✅ Parse para dialects custom +- ✅ Draw para gráficos en editor/panel + +### Denominación + +- Funciones: `kebab-case` (e.g., `make-node`, `compile-body`) +- Variables locales: `camelCase` (e.g., `isConnected`, `outputPort`) +- Constantes: `SCREAMING_SNAKE_CASE` (e.g., `DEFAULT_WIDTH`) +- Dialectos: `kebab-case` (e.g., `block-def`, `qvi-diagram`) +- Palabras clave de dialecto: sin prefijo (e.g., `emit`, `meta`, `connector`) + +--- + +## Recursos + +- **Docs Red oficial:** https://doc.red-lang.org +- **Red/View:** https://doc.red-lang.org/en/view.html +- **Red/Draw:** https://doc.red-lang.org/en/view.html#_draw-dialect +- **Red/Parse:** https://doc.red-lang.org/en/parse.html +- **TCP API:** Ver `docs/tcp-api.md` (específico de QTorres) +- **Skill Red en QTorres:** Este fichero + +## Cuándo consultar esta skill + +1. **Antes de escribir cualquier código Red** — especialmente Draw y View +2. **Cuando dudes de sintaxis** — tipo de datos, bloques, parse +3. **Para entender patrones idiomáticos** — composición, contextos, funciones +4. **Para TCP** — antes de implementar bloques de hardware (Fase 4) + +--- + +*Última actualización: 2026-04-20* +*Próxima: Fase 4 — bloques genéricos TCP/IP y USBTMC (sin bloques específicos por protocolo)* diff --git a/src/compiler/compiler-emit.red b/src/compiler/compiler-emit.red index d9077de..0265f1c 100644 --- a/src/compiler/compiler-emit.red +++ b/src/compiler/compiler-emit.red @@ -49,7 +49,7 @@ bind-emit: func [ block? item [ append/only result bind-emit item bindings ] - true [append result item] + true [append/only result item] ] ] result @@ -84,8 +84,10 @@ build-bindings: func [ ] foreach p bdef/inputs [ + found: false foreach w diagram/wires [ if all [w/to-node = node/id (to-word w/to-port) = p/name] [ + found: true case [ w/from-node < 0 [ _var: either w/from-node = -1 [ @@ -115,6 +117,10 @@ build-bindings: func [ ] ] ] + if all [not found not none? p/default] [ + append bindings p/name + append/only bindings p/default + ] ] foreach cfg bdef/configs [ diff --git a/src/graph/blocks.red b/src/graph/blocks.red index 2eab8c9..c240eca 100644 --- a/src/graph/blocks.red +++ b/src/graph/blocks.red @@ -29,7 +29,7 @@ block: func [ name [word! lit-word!] category [word! lit-word!] body [block!] - /local entry n cat port-name port-type cfg-type cfg-default emit-body + /local entry n cat port-name port-type port-default cfg-type cfg-default emit-body ][ n: to-word name cat: to-word category @@ -43,8 +43,10 @@ block: func [ ] parse body [ any [ - ['in set port-name word! set port-type lit-word!] - (append entry/inputs make object! [name: port-name type: port-type]) + ['in set port-name word! set port-type lit-word! + (port-default: none) + opt [ahead [string! | integer! | float! | logic!] set port-default skip]] + (append entry/inputs make object! [name: port-name type: port-type default: port-default]) | ['out set port-name word! set port-type lit-word!] (append entry/outputs make object! [name: port-name type: port-type]) | ['config set port-name word! set cfg-type lit-word! set cfg-default skip] @@ -379,4 +381,85 @@ block 'subvi 'function [ ; El emit se maneja en compile-subvi-call en compiler.red ] +; ── Hardware: TCP/IP (Fase 4 — Issue #19) ─────────────────────────────────── +; Bloques TCP al estilo LabVIEW: connection refnum encadena nodos (dataflow). +; host/port/timeout/bytes-to-read son puertos de entrada (cableables desde FP +; o desde constantes), igual que en los nodos TCP nativos de LabVIEW. +; Sin cluster de errores por ahora (se añadirá con VISA en Fase 4-E). + +_make-tcp-connection: func [a? h p] [ + make object! [active?: a? host: h port: p] +] + +_tcp-open-helper: func [host port timeout-ms /local ok] [ + ok: tcp/connect host to-integer port + _make-tcp-connection ok host to-integer port +] + +_tcp-write-helper: func [conn data /local bytes] [ + if not conn/active? [return reduce [conn 0]] + bytes: length? to-binary data + tcp/send data + reduce [conn bytes] +] + +_tcp-read-helper: func [conn sz timeout-ms /local buf bytes] [ + if not conn/active? [return reduce [conn "" 0]] + tcp/set-timeout to-integer timeout-ms + buf: tcp/receive to-integer sz + either buf [ + bytes: length? buf + reduce [conn to string! buf bytes] + ][ + reduce [conn "" 0] + ] +] + +_tcp-close-helper: func [conn] [ + if not conn/active? [return conn] + tcp/close + _make-tcp-connection false conn/host conn/port +] + +block 'tcp-open 'hardware [ + in address 'string "localhost" + in remote-port 'number 5000 + in timeout-ms 'number 60000 + out connection-out 'tcp-connection + emit [connection-out: _tcp-open-helper address remote-port timeout-ms] +] + +block 'tcp-write 'hardware [ + in connection-in 'tcp-connection + in data 'string "" + out connection-out 'tcp-connection + out bytes-written 'number + emit [ + _w: _tcp-write-helper connection-in data + connection-out: _w/1 + bytes-written: _w/2 + ] +] + +block 'tcp-read 'hardware [ + in connection-in 'tcp-connection + in bytes-to-read 'number 256 + in timeout-ms 'number 60000 + out connection-out 'tcp-connection + out data 'string + out bytes-read 'number + emit [ + _r: _tcp-read-helper connection-in bytes-to-read timeout-ms + connection-out: _r/1 + data: _r/2 + bytes-read: _r/3 + ] +] + +block 'tcp-close 'hardware [ + in connection-in 'tcp-connection + out connection-out 'tcp-connection + emit [connection-out: _tcp-close-helper connection-in] +] + #include %../compiler/compiler.red diff --git a/src/io/file-io-serialize.red b/src/io/file-io-serialize.red index 1f04051..dfa489c 100644 --- a/src/io/file-io-serialize.red +++ b/src/io/file-io-serialize.red @@ -408,7 +408,12 @@ format-qvi: func [ "if not _saved-qtorres-runtime [unset 'qtorres-runtime]^/^/" ]] "; --- Helpers de runtime ---^/" - "arr-subset-helper: func [arr st ln] [copy/part skip arr to-integer st to-integer ln]^/^/" + "arr-subset-helper: func [arr st ln] [copy/part skip arr to-integer st to-integer ln]^/" + "_make-tcp-connection: func [a? h p] [make object! [active?: a? host: h port: p]]^/" + "_tcp-open-helper: func [host port timeout-ms /local ok] [ok: tcp/connect host to-integer port _make-tcp-connection ok host to-integer port]^/" + "_tcp-write-helper: func [conn data /local bytes] [if not conn/active? [return reduce [conn 0]] bytes: length? to-binary data tcp/send data reduce [conn bytes]]^/" + "_tcp-read-helper: func [conn sz timeout-ms /local buf bytes] [if not conn/active? [return reduce [conn {} 0]] tcp/set-timeout to-integer timeout-ms buf: tcp/receive to-integer sz either buf [bytes: length? buf reduce [conn to string! buf bytes]] [reduce [conn {} 0]]]^/" + "_tcp-close-helper: func [conn] [if not conn/active? [return conn] tcp/close _make-tcp-connection false conn/host conn/port]^/^/" "; --- CÓDIGO GENERADO — no editar, se regenera al guardar ---^/" func-name ": context [^/" " exec: func [] [^/" ; TODO: extraer parámetros del connector @@ -431,7 +436,12 @@ format-qvi: func [ "if not _saved-qtorres-runtime [unset 'qtorres-runtime]^/^/" ]] "; --- Helpers de runtime ---^/" - "arr-subset-helper: func [arr st ln] [copy/part skip arr to-integer st to-integer ln]^/^/" + "arr-subset-helper: func [arr st ln] [copy/part skip arr to-integer st to-integer ln]^/" + "_make-tcp-connection: func [a? h p] [make object! [active?: a? host: h port: p]]^/" + "_tcp-open-helper: func [host port timeout-ms /local ok] [ok: tcp/connect host to-integer port _make-tcp-connection ok host to-integer port]^/" + "_tcp-write-helper: func [conn data /local bytes] [if not conn/active? [return reduce [conn 0]] bytes: length? to-binary data tcp/send data reduce [conn bytes]]^/" + "_tcp-read-helper: func [conn sz timeout-ms /local buf bytes] [if not conn/active? [return reduce [conn {} 0]] tcp/set-timeout to-integer timeout-ms buf: tcp/receive to-integer sz either buf [bytes: length? buf reduce [conn to string! buf bytes]] [reduce [conn {} 0]]]^/" + "_tcp-close-helper: func [conn] [if not conn/active? [return conn] tcp/close _make-tcp-connection false conn/host conn/port]^/^/" "; --- CÓDIGO GENERADO — no editar, se regenera al guardar ---^/" "either empty? system/options/args [^/" " view layout [^/" diff --git a/src/ui/diagram/canvas-dialogs.red b/src/ui/diagram/canvas-dialogs.red index ceec774..7234c39 100644 --- a/src/ui/diagram/canvas-dialogs.red +++ b/src/ui/diagram/canvas-dialogs.red @@ -391,6 +391,11 @@ open-palette: func [face x y /struct target-struct text "Cluster:" return button 80 "Bundle" [palette-add-node 'bundle] button 80 "Unbundle" [palette-add-node 'unbundle] return + text "Hardware (TCP):" return + button 80 "Open" [palette-add-node 'tcp-open] + button 80 "Write" [palette-add-node 'tcp-write] + button 80 "Read" [palette-add-node 'tcp-read] + button 80 "Close" [palette-add-node 'tcp-close] return text "Estructuras:" return button 80 "While" [palette-add-structure 'while-loop] button 80 "For" [palette-add-structure 'for-loop] diff --git a/src/ui/diagram/canvas-render.red b/src/ui/diagram/canvas-render.red index ebb550d..1462ac6 100644 --- a/src/ui/diagram/canvas-render.red +++ b/src/ui/diagram/canvas-render.red @@ -17,10 +17,12 @@ col-grid: 200.203.212 col-block-ctrl: 50.100.180 col-block-ind: 175.125.20 col-block-op: 55.75.105 +col-block-hw: 20.130.130 ; turquesa oscuro — bloques hardware (TCP, USBTMC, serie, DAQ) col-wire: 195.95.20 col-wire-bool: 20.160.20 col-wire-str: 220.100.160 col-wire-cluster: 139.69.19 +col-wire-session: 107.142.35 ; verde oliva — VISA session (estilo LabVIEW) col-wire-sel: 0.160.200 col-port-in: 50.110.200 col-port-out: 195.80.25 @@ -51,10 +53,11 @@ text-dy: either system/platform = 'Linux [8] [0] block-color: func [node-type /local cat] [ cat: block-category to-word node-type case [ - cat = 'input [col-block-ctrl] - cat = 'output [col-block-ind] - cat = 'cluster [col-wire-cluster] - true [col-block-op] + cat = 'input [col-block-ctrl] + cat = 'output [col-block-ind] + cat = 'cluster [col-wire-cluster] + cat = 'hardware [col-block-hw] + true [col-block-op] ] ] @@ -162,11 +165,12 @@ port-in-type: func [node port-name /local bdef p] [ ; Devuelve el color de wire para un tipo de dato. wire-data-color: func [data-type] [ case [ - data-type = 'boolean [col-wire-bool] - data-type = 'string [col-wire-str] - data-type = 'cluster [col-wire-cluster] - data-type = 'array [col-wire] ; mismo naranja que number, diferenciado por línea doble - true [col-wire] + data-type = 'boolean [col-wire-bool] + data-type = 'string [col-wire-str] + data-type = 'cluster [col-wire-cluster] + data-type = 'array [col-wire] ; mismo naranja que number, diferenciado por línea doble + data-type = 'tcp-session [col-wire-session] + true [col-wire] ] ] diff --git a/tests/test-blocks.red b/tests/test-blocks.red index 83ebd43..226592d 100644 --- a/tests/test-blocks.red +++ b/tests/test-blocks.red @@ -4,7 +4,7 @@ do %../src/graph/model.red ; model.red incluye blocks.red y ahora también make suite "blocks — registro" -assert "registra 41 bloques (34 + bundle + unbundle + cluster-control + cluster-indicator + waveform-chart + waveform-graph + subvi)" (41 = length? block-registry) +assert "registra 45 bloques (41 + tcp-open + tcp-write + tcp-read + tcp-close)" (45 = length? block-registry) assert "const está en el registro" (not none? find-block 'const) assert "add está en el registro" (not none? find-block 'add) assert "find-block devuelve none para bloques inexistentes" (none? find-block 'nonexistent) @@ -114,7 +114,7 @@ assert "to-string emit es [result: form a]" ([result: form a] suite "blocks — cluster: registro" -assert "registra 41 bloques (34 + bundle + unbundle + cluster-control + cluster-indicator + waveform-chart + waveform-graph + subvi)" (41 = length? block-registry) +assert "registra 45 bloques (41 + tcp-open + tcp-write + tcp-read + tcp-close)" (45 = length? block-registry) assert "bundle está en el registro" (not none? find-block 'bundle) assert "unbundle está en el registro" (not none? find-block 'unbundle) @@ -192,3 +192,138 @@ assert "waveform-graph name correcto" ("graph_1" = wg/name) assert "waveform-graph label/text correcto" ("Array" = wg/label/text) assert "waveform-graph value es block" (block? wg/value) assert "waveform-graph value vacío inicial" (empty? wg/value) + +; ══════════════════════════════════════════════════════════════════ +; Fase 4 — Issue #19: Bloques TCP estilo LabVIEW (connection refnum) +; ══════════════════════════════════════════════════════════════════ + +suite "blocks — tcp: registro" + +b-tcpo: find-block 'tcp-open +b-tcpw: find-block 'tcp-write +b-tcpr: find-block 'tcp-read +b-tcpx: find-block 'tcp-close + +assert "tcp-open está en el registro" (not none? b-tcpo) +assert "tcp-write está en el registro" (not none? b-tcpw) +assert "tcp-read está en el registro" (not none? b-tcpr) +assert "tcp-close está en el registro" (not none? b-tcpx) + +assert "tcp-open categoría es hardware" ('hardware = b-tcpo/category) +assert "tcp-write categoría es hardware" ('hardware = b-tcpw/category) +assert "tcp-read categoría es hardware" ('hardware = b-tcpr/category) +assert "tcp-close categoría es hardware" ('hardware = b-tcpx/category) + +suite "blocks — tcp: puertos (estilo LabVIEW)" + +; tcp-open: 3 entradas (address, remote-port, timeout-ms) → 1 salida (connection-out) +tcpo-in1: first b-tcpo/inputs +tcpo-in2: second b-tcpo/inputs +tcpo-in3: third b-tcpo/inputs +tcpo-out1: first b-tcpo/outputs + +assert "tcp-open tiene 3 entradas" (3 = length? b-tcpo/inputs) +assert "tcp-open tiene 1 salida" (1 = length? b-tcpo/outputs) +assert "tcp-open in[0] se llama address" ('address = tcpo-in1/name) +assert "tcp-open in[0] es string" ('string = tcpo-in1/type) +assert "tcp-open in[0] default es localhost" ("localhost" = tcpo-in1/default) +assert "tcp-open in[1] se llama remote-port" ('remote-port = tcpo-in2/name) +assert "tcp-open in[1] es number" ('number = tcpo-in2/type) +assert "tcp-open in[1] default es 5000" (5000 = tcpo-in2/default) +assert "tcp-open in[2] se llama timeout-ms" ('timeout-ms = tcpo-in3/name) +assert "tcp-open in[2] default es 60000" (60000 = tcpo-in3/default) +assert "tcp-open salida se llama connection-out" ('connection-out = tcpo-out1/name) +assert "tcp-open salida es tcp-connection" ('tcp-connection = tcpo-out1/type) +assert "tcp-open no tiene configs" (0 = length? b-tcpo/configs) + +; tcp-write: connection-in + data → connection-out + bytes-written +tcpw-in1: first b-tcpw/inputs +tcpw-in2: second b-tcpw/inputs +tcpw-out1: first b-tcpw/outputs +tcpw-out2: second b-tcpw/outputs + +assert "tcp-write tiene 2 entradas" (2 = length? b-tcpw/inputs) +assert "tcp-write tiene 2 salidas" (2 = length? b-tcpw/outputs) +assert "tcp-write in[0] se llama connection-in" ('connection-in = tcpw-in1/name) +assert "tcp-write in[0] es tcp-connection" ('tcp-connection = tcpw-in1/type) +assert "tcp-write in[1] se llama data" ('data = tcpw-in2/name) +assert "tcp-write in[1] es string" ('string = tcpw-in2/type) +assert "tcp-write out[0] se llama connection-out" ('connection-out = tcpw-out1/name) +assert "tcp-write out[0] es tcp-connection" ('tcp-connection = tcpw-out1/type) +assert "tcp-write out[1] se llama bytes-written" ('bytes-written = tcpw-out2/name) +assert "tcp-write out[1] es number" ('number = tcpw-out2/type) +assert "tcp-write no tiene configs" (0 = length? b-tcpw/configs) + +; tcp-read: connection-in + bytes-to-read + timeout-ms → connection-out + data + bytes-read +tcpr-in1: first b-tcpr/inputs +tcpr-in2: second b-tcpr/inputs +tcpr-in3: third b-tcpr/inputs +tcpr-out1: first b-tcpr/outputs +tcpr-out2: second b-tcpr/outputs +tcpr-out3: third b-tcpr/outputs + +assert "tcp-read tiene 3 entradas" (3 = length? b-tcpr/inputs) +assert "tcp-read tiene 3 salidas" (3 = length? b-tcpr/outputs) +assert "tcp-read in[0] se llama connection-in" ('connection-in = tcpr-in1/name) +assert "tcp-read in[1] se llama bytes-to-read" ('bytes-to-read = tcpr-in2/name) +assert "tcp-read in[1] default es 256" (256 = tcpr-in2/default) +assert "tcp-read in[2] se llama timeout-ms" ('timeout-ms = tcpr-in3/name) +assert "tcp-read in[2] default es 60000" (60000 = tcpr-in3/default) +assert "tcp-read out[0] se llama connection-out" ('connection-out = tcpr-out1/name) +assert "tcp-read out[1] se llama data" ('data = tcpr-out2/name) +assert "tcp-read out[1] es string" ('string = tcpr-out2/type) +assert "tcp-read out[2] se llama bytes-read" ('bytes-read = tcpr-out3/name) +assert "tcp-read no tiene configs" (0 = length? b-tcpr/configs) + +; tcp-close: connection-in → connection-out +tcpx-in1: first b-tcpx/inputs +tcpx-out1: first b-tcpx/outputs + +assert "tcp-close tiene 1 entrada" (1 = length? b-tcpx/inputs) +assert "tcp-close tiene 1 salida" (1 = length? b-tcpx/outputs) +assert "tcp-close in[0] se llama connection-in" ('connection-in = tcpx-in1/name) +assert "tcp-close in[0] es tcp-connection" ('tcp-connection = tcpx-in1/type) +assert "tcp-close out[0] se llama connection-out" ('connection-out = tcpx-out1/name) +assert "tcp-close out[0] es tcp-connection" ('tcp-connection = tcpx-out1/type) +assert "tcp-close no tiene configs" (0 = length? b-tcpx/configs) + +suite "blocks — tcp: emit (helpers connection refnum)" + +assert "tcp-open tiene emit" (block? b-tcpo/emit) +assert "tcp-write tiene emit" (block? b-tcpw/emit) +assert "tcp-read tiene emit" (block? b-tcpr/emit) +assert "tcp-close tiene emit" (block? b-tcpx/emit) + +assert "tcp-open emit usa _tcp-open-helper" (not none? find mold b-tcpo/emit "_tcp-open-helper") +assert "tcp-write emit usa _tcp-write-helper" (not none? find mold b-tcpw/emit "_tcp-write-helper") +assert "tcp-read emit usa _tcp-read-helper" (not none? find mold b-tcpr/emit "_tcp-read-helper") +assert "tcp-close emit usa _tcp-close-helper" (not none? find mold b-tcpx/emit "_tcp-close-helper") + +assert "tcp-read emit asigna connection-out" (not none? find mold b-tcpr/emit "connection-out") +assert "tcp-read emit asigna data" (not none? find mold b-tcpr/emit "data") +assert "tcp-read emit asigna bytes-read" (not none? find mold b-tcpr/emit "bytes-read") +assert "tcp-write emit asigna bytes-written" (not none? find mold b-tcpw/emit "bytes-written") + +suite "blocks — tcp: helpers runtime definidos" + +assert "_make-tcp-connection está definido" (function? :_make-tcp-connection) +assert "_tcp-open-helper está definido" (function? :_tcp-open-helper) +assert "_tcp-write-helper está definido" (function? :_tcp-write-helper) +assert "_tcp-read-helper está definido" (function? :_tcp-read-helper) +assert "_tcp-close-helper está definido" (function? :_tcp-close-helper) + +; _make-tcp-connection construye objeto con los campos correctos +_conn-test: _make-tcp-connection true "localhost" 5000 +assert "_make-tcp-connection activa?" (true = _conn-test/active?) +assert "_make-tcp-connection host" ("localhost" = _conn-test/host) +assert "_make-tcp-connection port" (5000 = _conn-test/port) + +; helpers con conexión inactiva son no-op +_conn-off: _make-tcp-connection false "x" 0 +_w-noop: _tcp-write-helper _conn-off "ignored" +assert "_tcp-write-helper no-op devuelve bloque" (block? _w-noop) +assert "_tcp-write-helper no-op bytes-written es 0" (0 = _w-noop/2) +assert "_tcp-close-helper no-op si inactiva" (not _conn-off/active?) +_r-noop: _tcp-read-helper _conn-off 64 100 +assert "_tcp-read-helper no-op devuelve data vacío" ("" = _r-noop/2) +assert "_tcp-read-helper no-op bytes-read es 0" (0 = _r-noop/3)