vendingzets-agent
Agente IoT para máquinas expendedoras. Corre en una Raspberry Pi conectada al bus MDB de la máquina, escucha las ventas y las reporta al sistema Vending Zets, que descuenta el stock del slot correspondiente.
Solo lee el bus. Nunca escribe: si la Pi se apaga o falla, la máquina sigue vendiendo exactamente igual.
Instalación
pip install vendingzets-agent
Requiere Python 3.11 o superior (Raspberry Pi OS Lite bookworm ya lo trae).
El paquete incluye el unit de systemd, el dispatcher de NetworkManager y las dos configuraciones de ejemplo. Para extraerlos:
vendingzets-agent files # lista qué trae y a dónde va cada uno
vendingzets-agent files --copy /tmp/vz # los saca con sus permisos correctos
Configuración
La API key se genera en el panel, en el detalle de la máquina, y se muestra una sola vez. Identifica a esa máquina: el agente no manda ningún otro identificador.
/etc/vendingzets/agent.toml:
[agent]
api_base_url = "https://vendingzets-production.up.railway.app/api/v1"
serial_port = "/dev/ttyAMA0"
queue_path = "/var/lib/vendingzets/queue.db"
# Número de selección del VMC -> código de slot en el sistema.
[slots]
1 = "A1"
2 = "A2"
3 = "B1"
# Solo para ventas en EFECTIVO, donde el bus no dice qué producto se eligió
# (ver "Limitación conocida"). Sirve si cada precio identifica un único slot.
[prices]
"0.75" = "A1"
"1.00" = "B1"
La key va por entorno, no en el archivo:
export VENDINGZETS_API_KEY="vz_live_..."
Uso
vendingzets-agent check # ¿la credencial sirve? ¿llega al backend?
vendingzets-agent run # escucha el bus y reporta ventas
vendingzets-agent run --simulate # ventas sintéticas, sin hardware
vendingzets-agent queue # estado de la cola local
--simulate permite instalar la Pi, validar credencial, cola y mapeo de slots
antes de tener el HAT MDB. Ojo: las ventas simuladas se registran de verdad
en el sistema, así que conviene usarlo contra una máquina de prueba.
Cómo no se pierden ventas
Toda venta detectada se escribe primero en SQLite y recién después se intenta enviar. Si no hay internet, se queda en la cola y se reintenta.
Cada venta lleva un id generado por el agente que viaja en el POST. El backend
lo usa como clave de idempotencia: reintentar la misma venta mil veces nunca la
duplica (201 la primera vez, 200 en los reintentos).
El POST también lleva sold_at: la hora en que ocurrió la venta en la
máquina, no la del envío. Importa en máquinas sin internet permanente, donde
la cola puede vaciarse días después — sin ese campo el backend fecha todo con
su propio NOW() y una semana de ventas aterriza en el mismo minuto, con el
stock correcto pero los reportes por día y hora inservibles.
El backend rechaza un sold_at del futuro o de más de 90 días atrás, que es lo
que manda una Pi con el reloj corrido (sin RTC y sin red toma la hora del último
apagado). En ese caso el agente no descarta la venta: la reenvía sin fecha y
deja el problema de reloj en el log. Se pierde la hora real, no la venta.
Con más de una venta pendiente, el envío va en lote (POST /agent/sales/batch,
hasta batch_size por request). Con una sola, va por el endpoint de a una: el de
lote tiene un rate limit más bajo, pensado para pocas llamadas grandes, y el goteo
de una máquina con internet permanente lo agotaría.
El lote responde el veredicto de cada venta, y ahí está su valor: sin eso el
agente no sabría cuáles sacar de la cola y cuáles descartar. Si el request entero
falla no se da por enviada ninguna — las que sí se hayan aplicado del otro lado
vuelven como duplicate en el reintento, por el id que genera el agente.
Los errores se tratan distinto según si tienen arreglo:
| Respuesta | Qué significa | Qué hace el agente |
|---|---|---|
201 / 200 |
registrada / ya existía | la saca de la cola |
404 / 409 / 422 |
slot inexistente, sin stock, payload inválido | la descarta (reintentar no cambia nada) |
401 / 403 |
credencial mala o sin scope | la conserva y reintenta |
429, 5xx, red |
temporal | la conserva y reintenta |
En lote, el status de cada item dice lo mismo: created/duplicate salen de la
cola, rejected se descarta, failed se conserva para el próximo intento.
Máquinas sin internet propio
Cuando la máquina no tiene conexión y alguien pasa cada tanto a conectarla
(hotspot del teléfono), poné sync_mode = "opportunistic" y heartbeat_interval = 0.
El agente deja de reintentar cada 5 segundos las 24 horas — el intervalo se
duplica solo hasta offline_max_interval — y en el panel hay que marcar esa
máquina como Se sincroniza por visitas, para que el aviso de "sin actividad"
use un plazo de días en vez de los 30 minutos por defecto.
Dos piezas hacen que la visita no sea a ciegas:
1. Sincronizar apenas hay red. El script 90-vendingzets-sync le manda
SIGUSR1 al agente cuando NetworkManager levanta una interfaz, y el agente
vacía la cola en el acto en vez de esperar su backoff (hasta 5 minutos con la
persona parada al lado de la máquina):
vendingzets-agent files --copy /tmp/vz
sudo install -m 755 -o root -g root \
/tmp/vz/90-vendingzets-sync /etc/NetworkManager/dispatcher.d/
El archivo tiene que ser de root y no escribible por otros: si no, NetworkManager lo ignora en silencio.
Conviene además dejar guardada en cada Pi la misma red de flota, así cualquier técnico solo prende su hotspot y la máquina engancha sola:
sudo nmcli connection add type wifi con-name vzets-field ssid vzets-field \
wifi-sec.key-mgmt wpa-psk wifi-sec.psk 'CLAVE' \
connection.autoconnect yes connection.autoconnect-priority 20
2. Ver si funcionó. El agente sirve una página de estado en el puerto
status_port (8099 por defecto) con las ventas pendientes, la hora del último
envío, el último error y un botón Sincronizar ahora. Desde el mismo teléfono
que da el hotspot:
http://<hostname>.local:8099
Para que ese nombre resuelva: sudo apt install avahi-daemon y un hostname por
máquina (sudo hostnamectl set-hostname vzets-a12). Sin avahi, por IP.
La página no pide autenticación — expone conteos de cola, nunca la API key ni
datos de venta, y su alcance es la red local del momento (el hotspot del propio
técnico). En una máquina conectada a una red que no controlás, status_port = 0
la desactiva.
Reloj: una Pi sin RTC arranca con la hora del último apagado, y esa hora
viaja en sold_at. Poné un RTC (DS3231) en las máquinas oportunistas, o al
menos verificá que fake-hwclock esté activo.
Limitación conocida: ventas en efectivo
El número de selección viaja por el bus solo cuando el pago pasa por el lector
cashless (tarjeta): ahí el VMC emite un VEND REQUEST con el ítem y el
precio, y luego un VEND SUCCESS.
Con pago en efectivo el VMC nunca publica qué ítem se eligió — el monedero y
el billetero solo reportan dinero entrando. Es una limitación del protocolo MDB,
no de este agente. Por eso existe [prices]: si en esa máquina cada precio
corresponde a un único slot, el monto alcanza para identificarlo. Si dos slots
comparten precio, la venta queda registrada en el log como no atribuible y no se
envía, porque el backend descuenta stock por slot_code.
Servicio del sistema
vendingzets-agent files --copy /tmp/vz
sudo cp /tmp/vz/vendingzets-agent.service /etc/systemd/system/
sudo systemctl enable --now vendingzets-agent
journalctl -u vendingzets-agent -f
El unit trae ExecStart=/usr/local/bin/vendingzets-agent: ajustá esa ruta a
donde haya quedado el ejecutable (which vendingzets-agent), que depende de si
instalaste con pip global, con un venv o con pipx.
Desarrollo
pip install -e ".[dev]"
pytest
Todo el protocolo y la cola se prueban sin hardware: el decodificador recibe bytes y emite eventos, así que una máquina expendedora se reemplaza por una lista de enteros.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vendingzets_agent-0.1.0.tar.gz.
File metadata
- Download URL: vendingzets_agent-0.1.0.tar.gz
- Upload date:
- Size: 35.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.11.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
553f78641e7fd0c97fcfb2b133ae1a725eb90f6f6092b2256244eb25362639de
|
|
| MD5 |
1393d19dac0d439caf6252acc96d861a
|
|
| BLAKE2b-256 |
6d1e3e830454a469ebfca9bf761854d4807925f291b08e9f8a4141c152c55447
|
File details
Details for the file vendingzets_agent-0.1.0-py3-none-any.whl.
File metadata
- Download URL: vendingzets_agent-0.1.0-py3-none-any.whl
- Upload date:
- Size: 35.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.11.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d40140d3a2429a3ba7b039040f8fe0104981be431e6c39d7e2978b00a192a03
|
|
| MD5 |
c3afe715ec6b7733309e2785fa3c69bb
|
|
| BLAKE2b-256 |
7e326c558025ed830da325d7b79e66085629baf1d265feb72bc094d9ff5c3e10
|