--[[ 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()