client2server-luv.lua 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. --[[
  2. client2server-luv - Event-driven using hotplug + ubus
  3. Features:
  4. - Listens to hotplug events (IMMEDIATE reaction)
  5. - Falls back to fast state checker if hotplug not available
  6. - Uses luv for async event loop
  7. Copyright (c) 2026 Luis Rosales - MIT License
  8. ]]
  9. local luv_ok, luv = pcall(require, "luv")
  10. local cfg = {
  11. server_url = os.getenv("SERVER_URL") or "http://163.245.193.47:3843",
  12. router_id = os.getenv("ROUTER_ID") or "unknown",
  13. state_file = "/tmp/client2server_state",
  14. event_fifo = "/tmp/client2server_events",
  15. }
  16. local function log_info(msg)
  17. os.execute('logger -t client2server -p user.info "' .. msg .. '"')
  18. end
  19. local function http_post(event_type, data)
  20. local json = '{"router_id":"' .. cfg.router_id .. '","event":"' .. event_type .. '","data":' .. data .. '}'
  21. local f = io.popen("curl -s -X POST '" .. cfg.server_url .. "/api/events' -H 'Content-Type: application/json' -d '" .. json .. "'", "r")
  22. local result = f:read("*a")
  23. f:close()
  24. return result
  25. end
  26. local function send_event(event_type, data)
  27. log_info("Event: " .. event_type)
  28. local resp = http_post(event_type, data)
  29. if resp and resp:match("OK") then
  30. log_info("OK")
  31. else
  32. log_info("Fail: " .. (resp or "nil"):sub(1, 50))
  33. end
  34. end
  35. -- State management
  36. local function load_state()
  37. local f = io.open(cfg.state_file, "r")
  38. if f then
  39. local content = f:read("*a")
  40. f:close()
  41. local clients = {}
  42. local dhcp = {}
  43. for line in content:gmatch("[^\n]+") do
  44. local t, v = line:match("^([^:]+):(.+)$")
  45. if t == "client" then
  46. local mac, iface = v:match("^(.+)|(.+)$")
  47. if mac then clients[mac] = {iface=iface} end
  48. elseif t == "dhcp" then
  49. local mac, ip = v:match("^(.+)->(.+)$")
  50. if mac and ip then dhcp[mac] = {ip=ip} end
  51. end
  52. end
  53. return { clients = clients, dhcp = dhcp }
  54. end
  55. return { clients = {}, dhcp = {} }
  56. end
  57. local function save_state(state)
  58. local f = io.open(cfg.state_file, "w")
  59. if f then
  60. for mac, info in pairs(state.clients) do
  61. f:write("client:" .. mac .. "|" .. (info.iface or "") .. "\n")
  62. end
  63. for mac, info in pairs(state.dhcp) do
  64. f:write("dhcp:" .. mac .. "->" .. info.ip .. "\n")
  65. end
  66. f:close()
  67. end
  68. end
  69. -- Create FIFO for hotplug events
  70. local function create_event_fifo()
  71. os.execute("mkfifo " .. cfg.event_fifo .. " 2>/dev/null")
  72. end
  73. -- Listen to hotplug events (event-driven!)
  74. local function listen_hotplug_events()
  75. log_info("Listening to hotplug events...")
  76. -- Method 1: Listen to /tmp/client2server_events FIFO
  77. local fifo = io.open(cfg.event_fifo, "r")
  78. if fifo then
  79. log_info("Reading from FIFO...")
  80. while true do
  81. local line = fifo:read("*line")
  82. if not line then break end
  83. log_info("Hotplug event: " .. line)
  84. -- Parse event
  85. local event_type = line:match("([^|]+)|")
  86. local data = line:match("|(.+)$") or "{}"
  87. send_event(event_type, data)
  88. end
  89. fifo:close()
  90. else
  91. log_info("FIFO not available, using fallback")
  92. end
  93. end
  94. -- Alternative: use ubus to subscribe to events
  95. local function subscribe_ubus_events()
  96. if not luv_ok then return false end
  97. log_info("Subscribing to ubus events...")
  98. -- Use a timer to check for new events in a file
  99. local timer = luv.new_timer()
  100. local last_check = 0
  101. local function check_hotplug_log()
  102. -- Read from system log for hotplug events
  103. local f = io.popen("logread -l 50 2>/dev/null | grep client2server-hotplug")
  104. if f then
  105. for line in f:lines() do
  106. if line:match("WiFi CONNECTED") then
  107. 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])")
  108. if mac then
  109. send_event("wifi_connected", '{"mac":"' .. mac .. '"}')
  110. end
  111. elseif line:match("WiFi DISCONNECTED") then
  112. 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])")
  113. if mac then
  114. send_event("wifi_disconnected", '{"mac":"' .. mac .. '"}')
  115. end
  116. elseif line:match("DHCP NEW") then
  117. 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])")
  118. local ip = line:match("-> (%d+%.%d+%.%d+%.%d+)")
  119. if mac and ip then
  120. send_event("dhcp_new", '{"mac":"' .. mac .. '","ip":"' .. ip .. '"}')
  121. end
  122. elseif line:match("DHCP RELEASE") then
  123. 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])")
  124. local ip = line:match("-> (%d+%.%d+%.%d+%.%d+)")
  125. if mac and ip then
  126. send_event("dhcp_release", '{"mac":"' .. mac .. '","ip":"' .. ip .. '"}')
  127. end
  128. end
  129. end
  130. f:close()
  131. end
  132. end
  133. -- Check every 1 second
  134. luv.timer_start(timer, 1000, 1000, check_hotplug_log)
  135. return true
  136. end
  137. -- Main
  138. function main()
  139. log_info("Starting client2server-luv...")
  140. log_info("Router: " .. cfg.router_id)
  141. log_info("Server: " .. cfg.server_url)
  142. if luv_ok then
  143. log_info("luv available - async mode")
  144. else
  145. log_info("luv NOT available")
  146. end
  147. -- Initial online event
  148. send_event("router_online", '{"status":"online"}')
  149. if luv_ok then
  150. -- Start listening to hotplug log
  151. subscribe_ubus_events()
  152. -- Run event loop
  153. luv.run()
  154. else
  155. while true do
  156. os.execute("sleep 60")
  157. end
  158. end
  159. end
  160. main()