MEMORY.md 5.8 KB

client2server — Project Memory

Bi-directional event forwarder: OpenWrt routers → Go server → Redpanda → LuIS backend.

Purpose

  • Routers push events (DHCP leases, WiFi connects/disconnects, WAN state) to a central server.
  • Two delivery paths: WebSocket long-lived connection for command roundtrips, and direct HTTP POST for hotplug-triggered events.
  • Server fans out via Redpanda topics; LuIS backend consumes.
  • Server can also push commands back to routers (uci_set, shell, reboot, wifi_restart, status).

Stack

Layer Tech
Router client Lua (OpenWrt), client2server-unified.lua
Hotplug paths Shell + curl, /etc/hotplug.d/{wireless,dhcp}/*
Transport WebSocket :3843 (Caddy LB → 2× Go servers)
API HTTP :3844 (Caddy → server1)
Event bus Redpanda (Kafka-compatible) :9092
Server Go (nhooyr.io/websocket, redpanda-data/redpanda-sdk-go)

Repo Layout (current)

client2server/
├── ARCHITECTURE.md      # Mermaid diagrams, full spec
├── README.md            # User-facing docs
├── MEMORY.md            # ← you are here
├── Caddyfile            # LB + reverse proxy
├── docker-compose.yml   # redpanda + 2× server + caddy
├── package/
│   ├── Makefile         # IPK build (includes hotplug)
│   ├── src/
│   │   └── client2server-unified.lua   # CANONICAL Lua client
│   ├── files/
│   │   ├── etc/config/client2server    # UCI defaults
│   │   └── etc/init.d/client2server    # Procd init script (exports UCI → env)
│   └── hotplug/
│       ├── 01-wifi      # wireless hotplug → wifi_connected/disconnected
│       └── 02-dhcp      # dhcp hotplug → dhcp_lease_new/expire
└── server/
    ├── main.go          # WS handler + Redpanda producer/consumer
    ├── go.mod
    └── Dockerfile

Removed in 2026-06-09 cleanup: package/src/{minimal,ws}.lua, legacy etc/ and usr/ (predecessor event-forwarder), server/{index,server-ws}.js (Node fallback). All in git history if needed.

Event Types (router → server)

Event Source Payload
dhcp_lease_new dnsmasq (luv timer + hotplug) mac, ip, hostname
dhcp_lease_expire dnsmasq (luv timer + hotplug) mac, old_ip
wan_link_up /sys/class/net/* device
wan_link_down /sys/class/net/* device
wan_dhcp_new ubus new_ip
wan_dhcp_changed ubus old_ip, new_ip
wifi_connected hostapd hotplug mac, interface
wifi_disconnected hostapd hotplug mac, interface

All event names are now consistent (hotplug scripts renamed to match unified.lua vocabulary on 2026-06-09).

Commands (server → router)

Command Args
uci_set config, section, option, value
shell command
reboot
wifi_restart
status

Ports

Service Port Notes
WebSocket LB 3843 Routers connect here
HTTP API 3844 REST + health
Redpanda Kafka 9092 Internal
Redpanda REST 8082 Schema/management
Redpanda Schema 8081 Schema registry

Quick Run

# Full stack
cd /root/.openclaw/workspace/client2server
TOKEN=*** docker-compose up -d

# Server only
cd server && go build -o server . && \
  REDPANDA_BROKERS=localhost:9092 TOKEN=*** ./server

# Install on router (manual)
scp package/src/client2server-unified.lua root@router:/usr/sbin/
scp package/files/etc/init.d/client2server root@router:/etc/init.d/
scp package/files/etc/config/client2server root@router:/etc/config/
scp package/hotplug/01-wifi root@router:/etc/hotplug.d/wireless/
scp package/hotplug/02-dhcp root@router:/etc/hotplug.d/dhcp/
ssh root@router "chmod +x /usr/sbin/client2server-unified.lua /etc/init.d/client2server /etc/hotplug.d/wireless/01-wifi /etc/hotplug.d/dhcp/02-dhcp"
ssh root@router "/etc/init.d/client2server enable && /etc/init.d/client2server start"

# Or build IPK
make package/client2server-unified/ipk

Architecture: Hybrid Event Delivery

The router has two parallel event paths to the server:

  1. Hotplug path (instant) — kernel fires, shell runs, curl POSTs

    • 01-wifi for WiFi connect/disconnect
    • 02-dhcp for DHCP lease add/del
    • Latency: ~10ms
  2. Lua state-diff path (≤1s) — luv async loop polls state, sends diffs

    • SSID name changes
    • wan_link_up/down
    • wan_dhcp_new/changed
    • dhcp_lease_new/expire (fallback / redundancy with hotplug)

Both paths post to the same POST /api/events endpoint, so server-side sees one event stream.

Environment Wiring (init.d → hotplug)

  • init.d/client2server reads UCI on start() and writes /var/run/client2server.env
  • Exports SERVER_URL, TOKEN, ROUTER_ID to Lua's environment
  • Hotplug scripts also uci get directly as fallback (in case called outside init.d context)
  • Env file removed on stop()

Client Evolution

The Lua client went through ~6 architectural rewrites in 2 days (Jun 7 2026) trying to kill polling. Final settled state = hybrid hotplug + luv state-diff. See git log 9e248cd..0b6d8ff for the iteration history.

Known Issues / TODO

  • ⚠️ Caddyfile email is commented out — set real value if enabling auto_https
  • ⚠️ UCI default wss://your-server.com/ws is a placeholder — must be edited per-deployment
  • ⚠️ Docker-compose TOKEN is the literal string *** — override via env or .env file
  • unified.lua is 759 lines — worth splitting into modules (DHCP/WiFi/WAN/WS/CMD) but functional as-is
  • No tests for the Go server; would benefit from integration tests using a mock WebSocket client
  • command_id idempotency + per-router command queue are server-side features (per commits 4134845, 7c85b33) — verify they work end-to-end

Author

Luis Rosales — MIT License 2026