Sobre el proyecto

## Descripción general plexus-python es el SDK ligero de Python de la plataforma Plexus. Plexus ofrece a los equipos de hardware almacenamiento de datos de series temporales y paneles: envía datos de drones, robots y dispositivos IoT a Plexus Time Series, o conéctate a una base de datos existente, para obtener paneles y alertas en tiempo real. Este paquete solo se encarga de enviar los datos a la pasarela de Plexus; el almacenamiento, los paneles, las alertas y la gestión de flotas residen en la plataforma. ## Inicio rápido ```bash pip install plexus-python ``` ```python from plexus import Plexus px = Plexus(api_key="plx_xxx", source_id="device-001") px.send("temperature", 72.5) ``` La clave de API se obtiene en app.plexus.company/api, o puedes ejecutar `plexus init` para autorizar este equipo desde el navegador. ## Identificación del dispositivo Cada dispositivo necesita un `source_id` único. Se recomienda configurarlo con el script de arranque, que exige indicar primero el nombre del dispositivo: ```bash curl -sL https://app.plexus.company/setup | bash -s -- \ --key plx_xxx --name drone-01 ``` El nombre se convierte en el `source_id` del dispositivo y debe coincidir con `^[a-z0-9][a-z0-9._-]*$` (máximo 256 caracteres). Si no hay ni `--name` ni `source_id=...` en el código, el SDK generará un id aleatorio en la primera ejecución (como `source-1a2b3c4d`) y lo guardará en `~/.plexus/config.json`. No uses el nombre de host: las imágenes de tarjeta SD clonadas arrancarán como `raspberrypi` y la telemetría se fusionará en el mismo source. Los nombres no se deduplican automáticamente; la pasarela devuelve tal cual el `source_id` declarado, y si dos dispositivos declaran el mismo nombre escribirán en el mismo source. ## Métodos principales ### send(metric, value) El método más usado, se llama cada vez que hay una nueva lectura. `metric` es una cadena de espacio de nombres con puntos (como `"motor.rpm"`), y `value` acepta cualquier tipo serializable a JSON: float/int (lecturas de sensores, contadores), str (máquinas de estado, códigos de error), bool (indicadores binarios), dict (vectores, lecturas estructuradas), list (formas de onda, ángulos de articulación). El parámetro opcional `tags={"motor_id": "A1"}` sirve para filtrar en los paneles, y `timestamp=t` especifica una marca de tiempo Unix en segundos. ### send_batch(points) Envía varias lecturas a la vez, compartiendo marca de tiempo y combinándolas en una sola llamada de red. `points` es una lista de tuplas `(metric, value)`, o tuplas de tres elementos `(metric, value, timestamp)` cuando se necesita una marca de tiempo por punto. ### batch() Úsalo cuando haya más de unas pocas lecturas por segundo. Cada `send()` es un mensaje WebSocket, y la pasarela limita el número de mensajes, no de puntos (2.000/s por conexión). Enviar 25 canales a 100 Hz uno a uno son 2.500 mensajes/s, y el exceso se descarta antes de almacenarse. ```python with px.batch(interval_ms=50) as b: while running: b.send("att.pos_x", att.x) ``` Un hilo en segundo plano vacía la cola cada `interval_ms`, y al salir del bloque se vacían los datos restantes; las lecturas conservan la marca de tiempo del momento de captura. Si la pasarela descarta fotogramas, se informa `RATE_LIMITED`; el SDK los cuenta en `px.rate_limited_frames` y lanza `RateLimitedError` en el siguiente envío. ### run(name) Un run es una ventana temporal con nombre sobre un source; se puede revisar en `/runs`, comparar alineado a T+0 y comprobar contra criterios de aprobación al cerrarse. Al salir del bloque, el run se marca como `completed`; si hay una excepción, se marca como `aborted` y se vuelve a lanzar. También puedes usar `px.start_run()` / `px.end_run()` por separado; `end_run()` devuelve el run con `test_result`. ### event(name, data) Sirve para registrar "cosas que ocurren" en lugar de "magnitudes que se miden continuamente": fallos, cambios de estado, acciones del operador, entradas de registro. La plataforma muestra los eventos como marcadores sobre el gráfico de telemetría, no como líneas de serie temporal. Límites por evento: valores de cadena de 256 bytes, valores dict/list de 4.096 bytes JSON, y como máximo 16 tags. ### Registros Este paquete no sube archivos de log ni incluye un `logging.Handler`. Para enviar líneas de log importantes a Plexus, usa eventos: `px.event("log", {"level": "error", "msg": "..."})`. Reenvía solo las líneas que quieras ver en la línea temporal (errores, advertencias, cambios de estado), no cada log de depuración. ## Streaming de vídeo El vídeo requiere un plan de pago: los fotogramas van por WebSocket y la pasarela los rechaza en el plan gratuito. Los fotogramas se reenvían en tiempo real a los espectadores y solo se almacenan al pulsar Record en la aplicación; una grabación dura como máximo 4 horas. - `send_video_frame(frame, camera_id)`: úsalo cuando controles tú el bucle de captura (callback de picamera2, bucle de OpenCV VideoCapture, tubería FFmpeg propia). Acepta numpy ndarray (requiere opencv-python), bytes JPEG (se pasan tal cual) y otros bytes de imagen (se decodifican con Pillow y se recodifican a JPEG, requiere `pip install plexus-python[video]`). - `stream_camera(url, camera_id)`: úsalo cuando tengas un flujo RTSP o un archivo de vídeo y no quieras gestionar el bucle de captura; el SDK ejecuta FFmpeg internamente (requiere FFmpeg en `$PATH`). Devuelve un `threading.Event`; llama a `.set()` para detenerlo, y se ejecuta en un hilo en segundo plano. ## Trae tu propio protocolo Este paquete no incluye adaptadores, autodetección ni demonios, solo el cliente. Usa las bibliotecas que ya conozcas para llevar valores a `px.send()`; el README incluye ejemplos de MAVLink (pymavlink), CAN (python-can), MQTT (paho-mqtt) y sensores I2C (Adafruit CircuitPython), con versiones ejecutables en `examples/`. ## Fiabilidad Cada envío se almacena primero en un búfer local y luego sale a la red, con reintentos de retroceso exponencial, conservando datos entre cortes de red. El búfer está en disco (SQLite) por defecto y sobrevive a reinicios y cortes de energía; con `persistent_buffer=False` es solo en memoria. Puedes usar `px.buffer_size()` y `px.flush_buffer()` para consultar el número de puntos y vaciar el búfer. ## Marcas de tiempo y corrección de reloj Por defecto, el SDK elige la hora por sí mismo. Con WebSocket, cada conexión se sincroniza con el reloj de la pasarela, de modo que los datos caen en el lugar correcto de la línea temporal aunque el reloj del sistema del dispositivo no sea exacto (primer arranque sin NTP, RTC caducado, imagen de sistema nueva). Cuando haya una fuente de tiempo externa fiable (GPS, RTC de confianza, NTP del host) o se reproduzcan datos históricos con marcas de tiempo conocidas, se debe pasar `timestamp` explícitamente. Limitaciones conocidas: la sincronización de reloj se refresca al reconectar el WebSocket, por lo que los dispositivos con conexiones largas y deriva del RTC acumulan deriva sin corregir entre reconexiones; la ruta de respaldo HTTP no recibe sincronización de reloj; `send_batch()` comparte una marca de tiempo por defecto. ## Transporte Por defecto se conecta a `/ws/device` de la pasarela mediante WebSocket, lo que da un flujo de telemetría de menor latencia y un canal para transportar acciones disparadas por el panel. Si el socket no está disponible, se recurre de forma transparente a `POST /ingest` sin perder datos. No hay selector de transporte; el SDK siempre prefiere WebSocket. En el plan gratuito la pasarela rechaza el WebSocket de dispositivo (`streaming_requires_plan`) y el SDK recurre por su cuenta a HTTP; `send()`, `send_batch()`, `batch()` y `event()` siguen funcionando; el streaming en tiempo real y el vídeo requieren un plan de pago, y el plan gratuito limita a 3 dispositivos y 7 días de historial. ## Comandos Puedes declarar qué comandos acepta tu código; debe hacerse antes del primer `send()` (la declaración se envía con el fotograma de autenticación). Decora los manejadores con `@px.command(...)`; los parámetros admiten string (maxLength, enum), integer/number (minimum, maximum, unit), boolean, además de title, description, default, required, con un máximo de 16 y planos, sin anidamiento. `danger` puede ser normal/dangerous/critical; si `idempotent` es verdadero, solo se reentrega tras una desconexión; `expires_in` va de 5 a 3600 segundos; `concurrency` puede ser accept/reject. El manejador se invoca como `handler(run, **params)`, con los parámetros ya validados y convertidos; el valor devuelto se convierte en el resultado del run, y una excepción hace que el run pase a `failed`. Se necesita una clave de API con permiso Receive commands. El SDK confirma cada run, no ejecuta dos veces el mismo run id, usa un reloj monótono para determinar la expiración y reproduce el estado no confirmado tras reconectar. `px.on_command()` está obsoleto pero sigue funcionando con la firma antigua. ## Variables de entorno `PLEXUS_API_KEY` (obligatoria), `PLEXUS_GATEWAY_URL` (por defecto `https://gateway.plexus.company`), `PLEXUS_GATEWAY_WS_URL` (por defecto `wss://gateway.plexus.company`). ## Agent skills El paquete incluye tres skills que enseñan a los agentes de código a usar la API de Plexus (endpoints, streaming en tiempo real, errores comunes que provocan 400 silenciosos). `plexus skills install` los instala en `~/.claude/skills`; con `--project` se instalan en `./.claude/skills` y viajan con el repositorio. Son Markdown puro, sin instalación ni credenciales. ## Arquitectura ``` Your code ── px.send() ── WebSocket /ws/device (o HTTP POST /ingest) ──> plexus-gateway ──> ClickHouse + Dashboard ``` Una ruta ligera, sin agent, sin demonio, sin adaptador. La plataforma HardwareOps completa (paneles, alertas, RCA, vista de flota) está en la interfaz web de app.plexus.company. ## Licencia Apache 2.0.