Skip to main content

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

vendingzets_agent-0.1.0.tar.gz (35.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

vendingzets_agent-0.1.0-py3-none-any.whl (35.7 kB view details)

Uploaded Python 3

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

Hashes for vendingzets_agent-0.1.0.tar.gz
Algorithm Hash digest
SHA256 553f78641e7fd0c97fcfb2b133ae1a725eb90f6f6092b2256244eb25362639de
MD5 1393d19dac0d439caf6252acc96d861a
BLAKE2b-256 6d1e3e830454a469ebfca9bf761854d4807925f291b08e9f8a4141c152c55447

See more details on using hashes here.

File details

Details for the file vendingzets_agent-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for vendingzets_agent-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1d40140d3a2429a3ba7b039040f8fe0104981be431e6c39d7e2978b00a192a03
MD5 c3afe715ec6b7733309e2785fa3c69bb
BLAKE2b-256 7e326c558025ed830da325d7b79e66085629baf1d265feb72bc094d9ff5c3e10

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page