agmen 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
agmen-0.2.0/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ /build*/
2
+ __pycache__/
3
+ *.pyc
4
+ .vscode/
5
+ .idea/
@@ -0,0 +1,66 @@
1
+ # Changelog
2
+
3
+ Todos los cambios importantes de Agmen se anotan aquí. Formato basado en
4
+ [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/); versiones según
5
+ [SemVer](https://semver.org/lang/es/). Política completa en
6
+ [`docs/VERSIONES.md`](docs/VERSIONES.md).
7
+
8
+ ## [Sin publicar]
9
+
10
+ ### Planeado (nivel 1, versión 0.3.0)
11
+ - Sobre v1 de 64 bytes + codificación `compact` + esquema en texto (RFC 0001).
12
+
13
+ ## [0.2.0] — 2026-10-30
14
+
15
+ **Todo el robot con un solo comando** y un panel web para controlarlo
16
+ (RFC 0003). Se acabaron las diez terminales.
17
+
18
+ ### Agregado
19
+ - Comando `agmen` (se instala con `pip install agmen`).
20
+ - `agmen run robot.toml`: lanza todos los nodos descritos en un archivo, junta
21
+ sus logs en una sola terminal con color por nodo, los reinicia si fallan
22
+ (`restart = "on-failure"`), compila antes si se pide (`before`) y con Ctrl+C
23
+ los detiene a todos de forma ordenada. Errores de configuración con
24
+ sugerencias ("¿Quisiste decir 'name'?").
25
+ - Panel web de control: estado de cada nodo con botones iniciar / detener /
26
+ reiniciar, logs de todos los nodos con filtros, gráficas de sensores y
27
+ **manejo del robot** con teclado (W A S D) o botones táctiles. Al soltar, el
28
+ robot frena.
29
+ - `agmen web`: el panel solo, sin administrar nodos.
30
+ - `examples/tutorial/robot.toml` y capítulo 5 del tutorial.
31
+ - Pruebas `scripts/run_smoke.py` en el CI.
32
+
33
+ ### Cambiado
34
+ - `tools/agmen-web/bridge.py` se reemplaza por `agmen web`, incluido en el paquete.
35
+
36
+ ### Corregido (encontrado por el primer CI en GitHub)
37
+ - Windows: los nodos de Python ya no truenan al imprimir acentos o "→" cuando
38
+ su salida va a un archivo u otro programa (`agmen.ensure_utf8_output()`).
39
+ - macOS: el panel ya no tarda en arrancar por una consulta DNS lenta.
40
+
41
+ ### Seguridad
42
+ - El panel rechaza peticiones de otros sitios (protección CSRF), recorta los
43
+ comandos de manejo a la velocidad máxima y limita su frecuencia.
44
+
45
+ ## [0.1.0] — 2026-10-30
46
+
47
+ Primera versión pública: **Nivel 0**.
48
+
49
+ ### Agregado
50
+ - Núcleo en C++20 (`#include <agmen/agmen.hpp>`, `namespace agmen::`) con CMake.
51
+ - Formato de cable v0: header de 32 bytes, `type_id` por hash del esquema y
52
+ errores claros (`agmen::wire::WireError`).
53
+ - Socket UDP multiplataforma (Windows/Linux/macOS) y sleep de alta resolución
54
+ en Windows.
55
+ - Ejemplos `publisher` y `subscriber` con medición de latencia y pérdidas.
56
+ - Paquete de Python `agmen` (implementación de referencia del formato).
57
+ - Prototipo de interfaz web `tools/agmen-web` (el navegador dibuja; el robot
58
+ solo publica).
59
+ - Tutorial: capítulos 0 a 4.
60
+ - RFC 0001 (formato de mensajes v1, **aceptado**) y RFC 0002 (interfaz web,
61
+ propuesta).
62
+ - CI en Windows (MSVC), Linux (GCC + sanitizers) y macOS (Clang).
63
+
64
+ [Sin publicar]: https://github.com/flkochu/Agmen/compare/v0.2.0...HEAD
65
+ [0.2.0]: https://github.com/flkochu/Agmen/releases/tag/v0.2.0
66
+ [0.1.0]: https://github.com/flkochu/Agmen/releases/tag/v0.1.0
agmen-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adán López Lozano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
agmen-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.5
2
+ Name: agmen
3
+ Version: 0.2.0
4
+ Summary: Agmen: el sistema nervioso para robótica, IA e IoT
5
+ Project-URL: Repository, https://github.com/flkochu/Agmen
6
+ Project-URL: Changelog, https://github.com/flkochu/Agmen/blob/main/CHANGELOG.md
7
+ Author: Adán López Lozano
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ai,iot,middleware,pubsub,robotica,robotics,ros
11
+ Classifier: Development Status :: 2 - Pre-Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: Natural Language :: Spanish
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Scientific/Engineering
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: tomli>=1.1; python_version < '3.11'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # agmen (Python)
24
+
25
+ **Agmen** es un framework de código abierto para robótica, IA e IoT. Su núcleo
26
+ (en C++) es el "sistema nervioso" del robot: mueve mensajes entre nodos de
27
+ forma rápida, confiable y con marca de tiempo.
28
+
29
+ Este paquete es la **implementación de referencia en Python** del formato de
30
+ mensajes. Con él, un nodo de Python habla con nodos de C++ sin dependencias
31
+ extra.
32
+
33
+ ```bash
34
+ pip install agmen
35
+ ```
36
+
37
+ ```python
38
+ from agmen import wire
39
+
40
+ msg = wire.ImuSample(accel=(0.0, 0.0, 9.81), gyro=(0.0, 0.0, 0.1))
41
+ packet = wire.encode(msg, seq=0, stamp_ns=wire.now_ns())
42
+ header, back = wire.decode(wire.ImuSample, packet)
43
+ ```
44
+
45
+ También instala el comando `agmen`, que levanta todo tu robot con un solo
46
+ comando y abre un panel web para controlarlo:
47
+
48
+ ```bash
49
+ agmen run robot.toml # todos los nodos + panel web (nodos, logs, sensores, manejar)
50
+ agmen web # solo el panel
51
+ ```
52
+
53
+ > ⚠ Versión 0.x: la API todavía va a cambiar entre versiones menores.
54
+
55
+ Documentación, tutorial y código: <https://github.com/flkochu/Agmen>
agmen-0.2.0/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,49 @@
1
+ # Paquete de Python `agmen` (pip install agmen).
2
+ #
3
+ # Hoy contiene la implementación de referencia del formato en Python puro.
4
+ # En el nivel 6 este mismo paquete se compilará desde el núcleo de C++
5
+ # (nanobind + scikit-build-core) sin cambiar de nombre.
6
+
7
+ [build-system]
8
+ requires = ["hatchling>=1.27"]
9
+ build-backend = "hatchling.build"
10
+
11
+ [project]
12
+ name = "agmen"
13
+ dynamic = ["version"]
14
+ description = "Agmen: el sistema nervioso para robótica, IA e IoT"
15
+ readme = "python/README.md"
16
+ requires-python = ">=3.10"
17
+ dependencies = [
18
+ "tomli>=1.1; python_version < '3.11'", # robot.toml en Python 3.10
19
+ ]
20
+ license = "MIT"
21
+ license-files = ["LICENSE"]
22
+ authors = [{ name = "Adán López Lozano" }]
23
+ keywords = ["robotics", "robotica", "middleware", "pubsub", "ros", "ai", "iot"]
24
+ classifiers = [
25
+ "Development Status :: 2 - Pre-Alpha",
26
+ "Intended Audience :: Developers",
27
+ "Intended Audience :: Science/Research",
28
+ "Natural Language :: Spanish",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Topic :: Scientific/Engineering",
32
+ "Topic :: Software Development :: Libraries",
33
+ ]
34
+
35
+ [project.scripts]
36
+ agmen = "agmen.cli:main"
37
+
38
+ [project.urls]
39
+ Repository = "https://github.com/flkochu/Agmen"
40
+ Changelog = "https://github.com/flkochu/Agmen/blob/main/CHANGELOG.md"
41
+
42
+ [tool.hatch.version]
43
+ path = "python/agmen/_version.py"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["python/agmen"]
47
+
48
+ [tool.hatch.build.targets.sdist]
49
+ include = ["python/agmen", "python/README.md", "LICENSE", "VERSION", "CHANGELOG.md"]
@@ -0,0 +1,33 @@
1
+ # agmen (Python)
2
+
3
+ **Agmen** es un framework de código abierto para robótica, IA e IoT. Su núcleo
4
+ (en C++) es el "sistema nervioso" del robot: mueve mensajes entre nodos de
5
+ forma rápida, confiable y con marca de tiempo.
6
+
7
+ Este paquete es la **implementación de referencia en Python** del formato de
8
+ mensajes. Con él, un nodo de Python habla con nodos de C++ sin dependencias
9
+ extra.
10
+
11
+ ```bash
12
+ pip install agmen
13
+ ```
14
+
15
+ ```python
16
+ from agmen import wire
17
+
18
+ msg = wire.ImuSample(accel=(0.0, 0.0, 9.81), gyro=(0.0, 0.0, 0.1))
19
+ packet = wire.encode(msg, seq=0, stamp_ns=wire.now_ns())
20
+ header, back = wire.decode(wire.ImuSample, packet)
21
+ ```
22
+
23
+ También instala el comando `agmen`, que levanta todo tu robot con un solo
24
+ comando y abre un panel web para controlarlo:
25
+
26
+ ```bash
27
+ agmen run robot.toml # todos los nodos + panel web (nodos, logs, sensores, manejar)
28
+ agmen web # solo el panel
29
+ ```
30
+
31
+ > ⚠ Versión 0.x: la API todavía va a cambiar entre versiones menores.
32
+
33
+ Documentación, tutorial y código: <https://github.com/flkochu/Agmen>
@@ -0,0 +1,29 @@
1
+ """Agmen para Python.
2
+
3
+ Por ahora contiene la implementación de referencia del formato de cable en
4
+ Python puro (`agmen.wire`). Más adelante este paquete se compilará desde el
5
+ núcleo de C++ (nanobind) y `agmen.wire` quedará como referencia y oráculo de pruebas.
6
+ """
7
+
8
+ import sys
9
+
10
+ from . import wire
11
+ from ._version import __version__
12
+
13
+ __all__ = ["wire", "__version__", "ensure_utf8_output"]
14
+
15
+
16
+ def ensure_utf8_output() -> None:
17
+ """Hace que print() nunca truene por acentos, "→" o "µs".
18
+
19
+ En Windows, cuando la salida va a un archivo o a otro programa (no a la
20
+ consola), Python usa la codificación vieja cp1252 y falla con
21
+ UnicodeEncodeError. Llama a esto al inicio de tu nodo.
22
+ """
23
+ for stream in (sys.stdout, sys.stderr):
24
+ if hasattr(stream, "reconfigure"):
25
+ try:
26
+ stream.reconfigure(encoding="utf-8", errors="replace")
27
+ except (ValueError, OSError):
28
+ pass
29
+
@@ -0,0 +1,7 @@
1
+ """Permite `python -m agmen run robot.toml` (igual que el comando `agmen`)."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,2 @@
1
+ # Debe coincidir con el archivo VERSION de la raíz (lo verifica scripts/check_version.py).
2
+ __version__ = "0.2.0"
@@ -0,0 +1,121 @@
1
+ """El comando `agmen`.
2
+
3
+ agmen run [robot.toml] lanza todo el robot + panel web, con un solo comando
4
+ agmen web solo el panel web (sin administrar nodos)
5
+ agmen version muestra la versión
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import signal
12
+ import sys
13
+ import threading
14
+
15
+ from ._version import __version__
16
+
17
+
18
+ def _wait_for_ctrl_c(stop: threading.Event) -> None:
19
+ def handler(signum, frame):
20
+ stop.set()
21
+
22
+ signal.signal(signal.SIGINT, handler)
23
+ signal.signal(signal.SIGTERM, handler)
24
+ if hasattr(signal, "SIGBREAK"): # Windows
25
+ signal.signal(signal.SIGBREAK, handler)
26
+ while not stop.wait(0.3):
27
+ pass
28
+
29
+
30
+ def cmd_run(args) -> int:
31
+ from .panel import Panel, Teleop
32
+ from .run import ConfigError, Launcher, load_config
33
+
34
+ try:
35
+ config = load_config(args.archivo)
36
+ except ConfigError as e:
37
+ print(f"error en {args.archivo}: {e}", file=sys.stderr)
38
+ return 2
39
+
40
+ launcher = Launcher(config)
41
+ launcher.say(f"Agmen {__version__} · robot '{config.name}' · {len(config.nodes)} nodos")
42
+ if not launcher.run_before():
43
+ return 1
44
+
45
+ panel = None
46
+ if config.web.enabled and not args.no_web:
47
+ teleop = Teleop(config.web.teleop.target, config.web.teleop.max_speed) if config.web.teleop else None
48
+ try:
49
+ panel = Panel(args.http or config.web.http, config.web.listen, config.name, launcher, teleop)
50
+ except OSError as e:
51
+ print(f"no se pudo abrir el panel web en {args.http or config.web.http}: {e}. "
52
+ f"¿Ya hay otro 'agmen run' corriendo? Usa --http 127.0.0.1:8081", file=sys.stderr)
53
+ return 1
54
+ panel.start(open_browser=config.web.open_browser and not args.no_browser)
55
+ launcher.say(f"panel web: {panel.url}")
56
+
57
+ launcher.start_all()
58
+ launcher.say("todo arrancado. Ctrl+C para detener todo.")
59
+ stop = threading.Event()
60
+ try:
61
+ _wait_for_ctrl_c(stop)
62
+ finally:
63
+ launcher.stop_all()
64
+ if panel:
65
+ panel.shutdown()
66
+ return 0
67
+
68
+
69
+ def cmd_web(args) -> int:
70
+ from .panel import Panel, Teleop
71
+
72
+ teleop = Teleop(args.teleop, 1.0) if args.teleop else None
73
+ try:
74
+ panel = Panel(args.http, args.listen, "", None, teleop)
75
+ except OSError as e:
76
+ print(f"no se pudo abrir el panel en {args.http}: {e}", file=sys.stderr)
77
+ return 1
78
+ panel.start(open_browser=not args.no_browser)
79
+ print(f"[agmen web] recibiendo Agmen en udp://{args.listen}")
80
+ print(f"[agmen web] abre {panel.url} en tu navegador")
81
+ if args.http.startswith("0.0.0.0"):
82
+ print("[agmen web] también accesible desde otros equipos de tu red (sin contraseña)")
83
+ stop = threading.Event()
84
+ try:
85
+ _wait_for_ctrl_c(stop)
86
+ finally:
87
+ panel.shutdown()
88
+ return 0
89
+
90
+
91
+ def main(argv: list[str] | None = None) -> int:
92
+ ap = argparse.ArgumentParser(prog="agmen", description="Agmen: el sistema nervioso para robótica, IA e IoT")
93
+ ap.add_argument("--version", action="version", version=f"agmen {__version__}")
94
+ sub = ap.add_subparsers(dest="cmd", required=True, metavar="comando")
95
+
96
+ p = sub.add_parser("run", help="lanza todos los nodos de robot.toml + el panel web")
97
+ p.add_argument("archivo", nargs="?", default="robot.toml", help="archivo del robot (default: robot.toml)")
98
+ p.add_argument("--no-web", action="store_true", help="no abrir el panel web")
99
+ p.add_argument("--no-browser", action="store_true", help="no abrir el navegador automáticamente")
100
+ p.add_argument("--http", help="dónde servir el panel (ej. 0.0.0.0:8080 para verlo desde el celular)")
101
+ p.set_defaults(func=cmd_run)
102
+
103
+ p = sub.add_parser("web", help="solo el panel web (sin administrar nodos)")
104
+ p.add_argument("--listen", default="127.0.0.1:7400", help="dónde recibir mensajes Agmen (UDP)")
105
+ p.add_argument("--http", default="127.0.0.1:8080", help="dónde servir el panel")
106
+ p.add_argument("--teleop", metavar="IP:PUERTO", help="habilita manejar el robot: destino de MotorCommand")
107
+ p.add_argument("--no-browser", action="store_true", help="no abrir el navegador automáticamente")
108
+ p.set_defaults(func=cmd_web)
109
+
110
+ p = sub.add_parser("version", help="muestra la versión")
111
+ p.set_defaults(func=lambda a: print(f"agmen {__version__}") or 0)
112
+
113
+ from . import ensure_utf8_output
114
+
115
+ ensure_utf8_output()
116
+ args = ap.parse_args(argv)
117
+ return args.func(args)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ sys.exit(main())
@@ -0,0 +1,5 @@
1
+ """Panel web de Agmen: el navegador dibuja, el robot solo publica."""
2
+
3
+ from .server import MotorCommand, Panel, Teleop
4
+
5
+ __all__ = ["Panel", "Teleop", "MotorCommand"]