| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- --[[
- client2server-luv - Event-driven using hotplug + ubus
-
- Features:
- - Listens to hotplug events (IMMEDIATE reaction)
- - Falls back to fast state checker if hotplug not available
- - Uses luv for async event loop
-
- Copyright (c) 2026 Luis Rosales - MIT License
- ]]
- local luv_ok, luv = pcall(require, "luv")
- local cfg = {
- server_url = os.getenv("SERVER_URL") or "http://163.245.193.47:3843",
- router_id = os.getenv("ROUTER_ID") or "unknown",
- state_file = "/tmp/client2server_state",
- event_fifo = "/tmp/client2server_events",
- }
- local function log_info(msg)
- os.execute('logger -t client2server -p user.info "' .. msg .. '"')
- end
- local function http_post(event_type, data)
- local json = '{"router_id":"' .. cfg.router_id .. '","event":"' .. event_type .. '","data":' .. data .. '}'
- local f = io.popen("curl -s -X POST '" .. cfg.server_url .. "/api/events' -H 'Content-Type: application/json' -d '" .. json .. "'", "r")
- local result = f:read("*a")
- f:close()
- return result
- end
- local function send_event(event_type, data)
- log_info("Event: " .. event_type)
- local resp = http_post(event_type, data)
- if resp and resp:match("OK") then
- log_info("OK")
- else
- log_info("Fail: " .. (resp or "nil"):sub(1, 50))
- end
- end
- -- State management
- local function load_state()
- local f = io.open(cfg.state_file, "r")
- if f then
- local content = f:read("*a")
- f:close()
- local clients = {}
- local dhcp = {}
- for line in content:gmatch("[^\n]+") do
- local t, v = line:match("^([^:]+):(.+)$")
- if t == "client" then
- local mac, iface = v:match("^(.+)|(.+)$")
- if mac then clients[mac] = {iface=iface} end
- elseif t == "dhcp" then
- local mac, ip = v:match("^(.+)->(.+)$")
- if mac and ip then dhcp[mac] = {ip=ip} end
- end
- end
- return { clients = clients, dhcp = dhcp }
- end
- return { clients = {}, dhcp = {} }
- end
- local function save_state(state)
- local f = io.open(cfg.state_file, "w")
- if f then
- for mac, info in pairs(state.clients) do
- f:write("client:" .. mac .. "|" .. (info.iface or "") .. "\n")
- end
- for mac, info in pairs(state.dhcp) do
- f:write("dhcp:" .. mac .. "->" .. info.ip .. "\n")
- end
- f:close()
- end
- end
- -- Create FIFO for hotplug events
- local function create_event_fifo()
- os.execute("mkfifo " .. cfg.event_fifo .. " 2>/dev/null")
- end
- -- Listen to hotplug events (event-driven!)
- local function listen_hotplug_events()
- log_info("Listening to hotplug events...")
-
- -- Method 1: Listen to /tmp/client2server_events FIFO
- local fifo = io.open(cfg.event_fifo, "r")
-
- if fifo then
- log_info("Reading from FIFO...")
- while true do
- local line = fifo:read("*line")
- if not line then break end
-
- log_info("Hotplug event: " .. line)
-
- -- Parse event
- local event_type = line:match("([^|]+)|")
- local data = line:match("|(.+)$") or "{}"
-
- send_event(event_type, data)
- end
- fifo:close()
- else
- log_info("FIFO not available, using fallback")
- end
- end
- -- Alternative: use ubus to subscribe to events
- local function subscribe_ubus_events()
- if not luv_ok then return false end
-
- log_info("Subscribing to ubus events...")
-
- -- Use a timer to check for new events in a file
- local timer = luv.new_timer()
- local last_check = 0
-
- local function check_hotplug_log()
- -- Read from system log for hotplug events
- local f = io.popen("logread -l 50 2>/dev/null | grep client2server-hotplug")
- if f then
- for line in f:lines() do
- if line:match("WiFi CONNECTED") then
- local mac = line:match("([a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9])")
- if mac then
- send_event("wifi_connected", '{"mac":"' .. mac .. '"}')
- end
- elseif line:match("WiFi DISCONNECTED") then
- local mac = line:match("([a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9])")
- if mac then
- send_event("wifi_disconnected", '{"mac":"' .. mac .. '"}')
- end
- elseif line:match("DHCP NEW") then
- local mac = line:match("([a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9])")
- local ip = line:match("-> (%d+%.%d+%.%d+%.%d+)")
- if mac and ip then
- send_event("dhcp_new", '{"mac":"' .. mac .. '","ip":"' .. ip .. '"}')
- end
- elseif line:match("DHCP RELEASE") then
- local mac = line:match("([a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9])")
- local ip = line:match("-> (%d+%.%d+%.%d+%.%d+)")
- if mac and ip then
- send_event("dhcp_release", '{"mac":"' .. mac .. '","ip":"' .. ip .. '"}')
- end
- end
- end
- f:close()
- end
- end
-
- -- Check every 1 second
- luv.timer_start(timer, 1000, 1000, check_hotplug_log)
-
- return true
- end
- -- Main
- function main()
- log_info("Starting client2server-luv...")
- log_info("Router: " .. cfg.router_id)
- log_info("Server: " .. cfg.server_url)
-
- if luv_ok then
- log_info("luv available - async mode")
- else
- log_info("luv NOT available")
- end
-
- -- Initial online event
- send_event("router_online", '{"status":"online"}')
-
- if luv_ok then
- -- Start listening to hotplug log
- subscribe_ubus_events()
-
- -- Run event loop
- luv.run()
- else
- while true do
- os.execute("sleep 60")
- end
- end
- end
- main()
|