main.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. // Beacon Daemon - collects events from scanners and uploads to server
  2. package main
  3. import (
  4. "context"
  5. "encoding/json"
  6. "flag"
  7. "fmt"
  8. "log"
  9. "net"
  10. "net/http"
  11. "os"
  12. "os/signal"
  13. "strings"
  14. "sync"
  15. "syscall"
  16. "time"
  17. "github.com/go-zeromq/zmq4"
  18. )
  19. const (
  20. defaultConfigPath = "/opt/mybeacon/etc/config.json"
  21. defaultStatePath = "/opt/mybeacon/etc/device.json"
  22. defaultBinDir = "/opt/mybeacon/bin"
  23. defaultWiFiIface = "wlan0"
  24. maxSpoolBytes = 100 * 1024 * 1024 // 100 MB
  25. )
  26. type Daemon struct {
  27. cfg *Config
  28. state *DeviceState
  29. client *APIClient
  30. spooler *Spooler
  31. sshTunnel *SSHTunnel
  32. dashboardTunnel *SSHTunnel
  33. scanners *ScannerManager
  34. api *APIServer
  35. netmgr *NetworkManager
  36. bleEvents []interface{}
  37. wifiEvents []interface{}
  38. mu sync.Mutex
  39. configPath string
  40. statePath string
  41. httpAddr string // HTTP API listen address
  42. stopChan chan struct{}
  43. // Upload failure counters (for reducing log spam)
  44. bleUploadFailures int
  45. wifiUploadFailures int
  46. }
  47. func main() {
  48. var (
  49. configPath = flag.String("config", defaultConfigPath, "Config file path")
  50. statePath = flag.String("state", defaultStatePath, "Device state file path")
  51. serverAddr = flag.String("server", "", "API server address (e.g., http://192.168.5.2:5000)")
  52. binDir = flag.String("bindir", defaultBinDir, "Directory with scanner binaries")
  53. wifiIface = flag.String("wifi-iface", defaultWiFiIface, "WiFi interface for monitor mode")
  54. httpAddr = flag.String("http", ":8080", "HTTP API listen address")
  55. debug = flag.Bool("debug", false, "Enable debug logging")
  56. )
  57. flag.Parse()
  58. log.SetFlags(log.Ltime)
  59. log.Println("================================================================================")
  60. log.Println("Beacon Daemon starting...")
  61. // Load configuration
  62. cfg, err := LoadConfig(*configPath)
  63. if err != nil {
  64. log.Printf("Warning: failed to load config: %v (using defaults)", err)
  65. cfg = DefaultConfig()
  66. }
  67. cfg.Debug = *debug || cfg.Debug
  68. // Override server address if provided
  69. if *serverAddr != "" {
  70. cfg.APIBase = *serverAddr + "/api/v1"
  71. log.Printf("Using server: %s", *serverAddr)
  72. }
  73. // Store WiFi interface
  74. cfg.WiFiIface = *wifiIface
  75. // Load device state
  76. state, err := LoadDeviceState(*statePath)
  77. if err != nil {
  78. log.Printf("Warning: failed to load state: %v", err)
  79. state = &DeviceState{}
  80. }
  81. // Get device ID from MAC if not set
  82. if state.DeviceID == "" {
  83. state.DeviceID = getDeviceID()
  84. SaveDeviceState(*statePath, state)
  85. }
  86. log.Printf("Device ID: %s", state.DeviceID)
  87. // Create spooler
  88. spooler, err := NewSpooler(cfg.SpoolDir, maxSpoolBytes)
  89. if err != nil {
  90. log.Fatalf("Failed to create spooler: %v", err)
  91. }
  92. // Create API client
  93. client := NewAPIClient(cfg.APIBase)
  94. // Create SSH tunnel managers (will be started when enabled in config)
  95. sshTunnelCfg := &TunnelConfig{
  96. Enabled: cfg.SSHTunnel.Enabled,
  97. Server: cfg.SSHTunnel.Server,
  98. Port: cfg.SSHTunnel.Port,
  99. User: cfg.SSHTunnel.User,
  100. RemotePort: cfg.SSHTunnel.RemotePort,
  101. KeepaliveInterval: cfg.SSHTunnel.KeepaliveInterval,
  102. ReconnectDelay: cfg.SSHTunnel.ReconnectDelay,
  103. }
  104. sshTunnel := NewSSHTunnel("ssh", 22, sshTunnelCfg)
  105. dashboardTunnelCfg := &TunnelConfig{
  106. Enabled: cfg.DashboardTunnel.Enabled,
  107. Server: cfg.DashboardTunnel.Server,
  108. Port: cfg.DashboardTunnel.Port,
  109. User: cfg.DashboardTunnel.User,
  110. RemotePort: cfg.DashboardTunnel.RemotePort,
  111. KeepaliveInterval: cfg.DashboardTunnel.KeepaliveInterval,
  112. ReconnectDelay: cfg.DashboardTunnel.ReconnectDelay,
  113. }
  114. dashboardTunnel := NewSSHTunnel("dashboard", 80, dashboardTunnelCfg)
  115. // Create scanner manager
  116. scanners := NewScannerManager(*binDir, cfg.Debug)
  117. // Create network manager (manages eth0, wlan0 client, wlan0 AP fallback)
  118. netmgr := NewNetworkManager(cfg, scanners, state.DevicePassword)
  119. // Create daemon
  120. daemon := &Daemon{
  121. cfg: cfg,
  122. state: state,
  123. client: client,
  124. spooler: spooler,
  125. sshTunnel: sshTunnel,
  126. dashboardTunnel: dashboardTunnel,
  127. scanners: scanners,
  128. netmgr: netmgr,
  129. configPath: *configPath,
  130. statePath: *statePath,
  131. httpAddr: *httpAddr,
  132. stopChan: make(chan struct{}),
  133. }
  134. // Create API server
  135. daemon.api = NewAPIServer(daemon)
  136. if state.DeviceToken != "" {
  137. daemon.client.SetToken(state.DeviceToken)
  138. }
  139. // Handle signals
  140. sigChan := make(chan os.Signal, 1)
  141. signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
  142. go func() {
  143. <-sigChan
  144. log.Println("Shutting down...")
  145. daemon.api.Stop()
  146. daemon.scanners.StopAll()
  147. daemon.sshTunnel.Stop()
  148. daemon.dashboardTunnel.Stop()
  149. daemon.netmgr.Stop()
  150. close(daemon.stopChan)
  151. }()
  152. // Start HTTP API server if enabled (dashboard should be available immediately)
  153. if cfg.Dashboard.Enabled {
  154. go func() {
  155. log.Printf("Starting HTTP API server on %s", daemon.httpAddr)
  156. if err := daemon.api.Start(daemon.httpAddr); err != nil && err != http.ErrServerClosed {
  157. log.Printf("HTTP server error: %v", err)
  158. }
  159. }()
  160. // Give HTTP server time to bind
  161. time.Sleep(100 * time.Millisecond)
  162. } else {
  163. log.Println("Dashboard disabled - HTTP server not started")
  164. }
  165. // Start network manager (manages eth0, wlan0 client, wlan0 AP fallback, WiFi scanner)
  166. // Network manager will automatically handle all network priorities and scanner coordination
  167. daemon.netmgr.Start()
  168. // Start SSH tunnel if enabled
  169. if cfg.SSHTunnel.Enabled && cfg.SSHTunnel.RemotePort != 0 {
  170. daemon.sshTunnel.Start()
  171. }
  172. // Start Dashboard tunnel if enabled
  173. if cfg.DashboardTunnel.Enabled && cfg.DashboardTunnel.RemotePort != 0 {
  174. daemon.dashboardTunnel.Start()
  175. }
  176. // Start BLE scanner if enabled (not managed by network manager)
  177. if cfg.BLE.Enabled {
  178. if !daemon.scanners.IsBLERunning() {
  179. log.Println("Starting BLE scanner...")
  180. if err := daemon.scanners.StartBLE(cfg.ZMQAddrBLE); err != nil {
  181. log.Printf("Failed to start BLE scanner: %v", err)
  182. }
  183. }
  184. }
  185. // Start registration loop (if not registered)
  186. go daemon.registrationLoop()
  187. // Start config polling loop
  188. go daemon.configLoop()
  189. // Start ZMQ subscribers
  190. go daemon.subscribeLoop("ble", cfg.ZMQAddrBLE)
  191. go daemon.subscribeLoop("wifi", cfg.ZMQAddrWiFi)
  192. // Start batch upload loops
  193. go daemon.uploadLoop("ble", "/ble", cfg.BLE.BatchIntervalMs)
  194. go daemon.uploadLoop("wifi", "/wifi", cfg.WiFi.BatchIntervalMs)
  195. // Start spool flush loop
  196. go daemon.spoolFlushLoop()
  197. // Wait for shutdown
  198. <-daemon.stopChan
  199. log.Println("Daemon stopped")
  200. }
  201. func (d *Daemon) registrationLoop() {
  202. for {
  203. select {
  204. case <-d.stopChan:
  205. return
  206. default:
  207. }
  208. if d.state.DeviceToken != "" {
  209. time.Sleep(60 * time.Second)
  210. continue
  211. }
  212. log.Println("Attempting device registration...")
  213. // Generate or load SSH key pair
  214. sshKeyPath := "/etc/beacon/ssh_tunnel_ed25519"
  215. sshPubKey, err := GenerateOrLoadSSHKey(sshKeyPath)
  216. if err != nil {
  217. log.Printf("Failed to generate/load SSH key: %v", err)
  218. time.Sleep(10 * time.Second)
  219. continue
  220. }
  221. log.Printf("SSH key ready: %s", sshKeyPath)
  222. req := &RegistrationRequest{
  223. DeviceID: d.state.DeviceID,
  224. SSHPublicKey: sshPubKey,
  225. }
  226. // Try to get IPs
  227. if ip := getInterfaceIP("eth0"); ip != "" {
  228. req.EthIP = &ip
  229. }
  230. if ip := getInterfaceIP("wlan0"); ip != "" {
  231. req.WlanIP = &ip
  232. }
  233. resp, err := d.client.Register(req)
  234. if err != nil {
  235. log.Printf("Registration failed: %v", err)
  236. time.Sleep(10 * time.Second)
  237. continue
  238. }
  239. d.state.DeviceToken = resp.DeviceToken
  240. d.state.DevicePassword = resp.DevicePassword
  241. d.client.SetToken(resp.DeviceToken)
  242. SaveDeviceState(d.statePath, d.state)
  243. log.Printf("Device registered, token received")
  244. // Immediately fetch config after registration
  245. log.Println("Fetching initial config from server...")
  246. d.fetchAndApplyConfig()
  247. }
  248. }
  249. func (d *Daemon) configLoop() {
  250. // Initial fetch immediately
  251. d.fetchAndApplyConfig()
  252. ticker := time.NewTicker(30 * time.Second)
  253. defer ticker.Stop()
  254. for {
  255. select {
  256. case <-d.stopChan:
  257. return
  258. case <-ticker.C:
  259. }
  260. d.fetchAndApplyConfig()
  261. }
  262. }
  263. func (d *Daemon) fetchAndApplyConfig() {
  264. if d.state.DeviceToken == "" {
  265. return
  266. }
  267. serverCfg, err := d.client.GetConfig(d.state.DeviceID)
  268. if err != nil {
  269. // In LAN mode, suppress errors (server might be unreachable)
  270. if d.cfg.Mode == "lan" {
  271. // Silent - use local config
  272. return
  273. }
  274. if d.cfg.Debug {
  275. log.Printf("Config fetch failed: %v", err)
  276. }
  277. return
  278. }
  279. // Determine effective mode: force_cloud overrides local mode setting
  280. effectiveMode := d.cfg.Mode
  281. if effectiveMode == "" {
  282. effectiveMode = "cloud"
  283. }
  284. if serverCfg.ForceCloud {
  285. if effectiveMode == "lan" {
  286. log.Println("Server force_cloud enabled - switching to cloud mode")
  287. }
  288. effectiveMode = "cloud"
  289. }
  290. d.mu.Lock()
  291. // Track changes for scanner restart
  292. bleChanged := false
  293. wifiMonitorChanged := false
  294. wifiClientChanged := false
  295. if effectiveMode == "cloud" {
  296. // Cloud mode: server settings have priority
  297. bleChanged = d.cfg.BLE.Enabled != serverCfg.BLE.Enabled
  298. wifiMonitorChanged = d.cfg.WiFi.MonitorEnabled != serverCfg.WiFi.MonitorEnabled
  299. wifiClientChanged = d.cfg.WiFi.ClientEnabled != serverCfg.WiFi.ClientEnabled ||
  300. d.cfg.WiFi.SSID != serverCfg.WiFi.SSID ||
  301. d.cfg.WiFi.PSK != serverCfg.WiFi.PSK
  302. d.cfg.BLE.Enabled = serverCfg.BLE.Enabled
  303. d.cfg.BLE.BatchIntervalMs = serverCfg.BLE.BatchIntervalMs
  304. d.cfg.WiFi.MonitorEnabled = serverCfg.WiFi.MonitorEnabled
  305. d.cfg.WiFi.ClientEnabled = serverCfg.WiFi.ClientEnabled
  306. d.cfg.WiFi.SSID = serverCfg.WiFi.SSID
  307. d.cfg.WiFi.PSK = serverCfg.WiFi.PSK
  308. d.cfg.WiFi.BatchIntervalMs = serverCfg.WiFi.BatchIntervalMs
  309. d.cfg.Debug = serverCfg.Debug
  310. // NTP from server in cloud mode
  311. if len(serverCfg.Net.NTP.Servers) > 0 {
  312. d.cfg.Network.NTPServers = serverCfg.Net.NTP.Servers
  313. }
  314. }
  315. // LAN mode: local settings have priority, we keep what's in d.cfg
  316. // Only tunnels come from server (for remote support)
  317. // SSH tunnel ALWAYS from server (for remote support access)
  318. sshChanged := d.cfg.SSHTunnel.Enabled != serverCfg.SSHTunnel.Enabled ||
  319. d.cfg.SSHTunnel.RemotePort != serverCfg.SSHTunnel.RemotePort
  320. if serverCfg.SSHTunnel.Enabled {
  321. d.cfg.SSHTunnel.Enabled = true
  322. d.cfg.SSHTunnel.Server = serverCfg.SSHTunnel.Server
  323. d.cfg.SSHTunnel.Port = serverCfg.SSHTunnel.Port
  324. d.cfg.SSHTunnel.User = serverCfg.SSHTunnel.User
  325. d.cfg.SSHTunnel.RemotePort = serverCfg.SSHTunnel.RemotePort
  326. d.cfg.SSHTunnel.KeepaliveInterval = serverCfg.SSHTunnel.KeepaliveInterval
  327. } else {
  328. d.cfg.SSHTunnel.Enabled = false
  329. }
  330. // Dashboard tunnel ALWAYS from server (for remote dashboard access)
  331. dashboardTunnelChanged := d.cfg.DashboardTunnel.Enabled != serverCfg.DashboardTunnel.Enabled ||
  332. d.cfg.DashboardTunnel.RemotePort != serverCfg.DashboardTunnel.RemotePort
  333. if serverCfg.DashboardTunnel.Enabled {
  334. d.cfg.DashboardTunnel.Enabled = true
  335. d.cfg.DashboardTunnel.Server = serverCfg.DashboardTunnel.Server
  336. d.cfg.DashboardTunnel.Port = serverCfg.DashboardTunnel.Port
  337. d.cfg.DashboardTunnel.User = serverCfg.DashboardTunnel.User
  338. d.cfg.DashboardTunnel.RemotePort = serverCfg.DashboardTunnel.RemotePort
  339. d.cfg.DashboardTunnel.KeepaliveInterval = serverCfg.DashboardTunnel.KeepaliveInterval
  340. } else {
  341. d.cfg.DashboardTunnel.Enabled = false
  342. }
  343. // Dashboard HTTP API ALWAYS from server (for remote management)
  344. dashboardChanged := d.cfg.Dashboard.Enabled != serverCfg.Dashboard.Enabled
  345. d.cfg.Dashboard.Enabled = serverCfg.Dashboard.Enabled
  346. d.mu.Unlock()
  347. // Apply BLE scanner changes (not managed by Network Manager)
  348. if bleChanged {
  349. log.Printf("BLE config changed (mode=%s): enabled=%v", effectiveMode, d.cfg.BLE.Enabled)
  350. if d.cfg.BLE.Enabled {
  351. if !d.scanners.IsBLERunning() {
  352. log.Println("Starting BLE scanner...")
  353. d.scanners.StartBLE(d.cfg.ZMQAddrBLE)
  354. }
  355. } else {
  356. if d.scanners.IsBLERunning() {
  357. log.Println("Stopping BLE scanner...")
  358. d.scanners.StopBLE()
  359. }
  360. }
  361. }
  362. // Log WiFi config changes (Network Manager will handle automatically)
  363. if wifiMonitorChanged || wifiClientChanged {
  364. log.Printf("WiFi config changed (mode=%s): monitor=%v client=%v ssid=%s",
  365. effectiveMode, d.cfg.WiFi.MonitorEnabled, d.cfg.WiFi.ClientEnabled, d.cfg.WiFi.SSID)
  366. log.Println("Network Manager will apply changes automatically")
  367. }
  368. // Update SSH tunnel config
  369. sshTunnelCfg := &TunnelConfig{
  370. Enabled: d.cfg.SSHTunnel.Enabled,
  371. Server: d.cfg.SSHTunnel.Server,
  372. Port: d.cfg.SSHTunnel.Port,
  373. User: d.cfg.SSHTunnel.User,
  374. RemotePort: d.cfg.SSHTunnel.RemotePort,
  375. KeepaliveInterval: d.cfg.SSHTunnel.KeepaliveInterval,
  376. ReconnectDelay: d.cfg.SSHTunnel.ReconnectDelay,
  377. }
  378. d.sshTunnel.UpdateConfig(sshTunnelCfg)
  379. if sshChanged {
  380. if d.cfg.SSHTunnel.Enabled && d.cfg.SSHTunnel.RemotePort != 0 {
  381. log.Printf("SSH tunnel enabled by server (port %d)", d.cfg.SSHTunnel.RemotePort)
  382. d.sshTunnel.Restart()
  383. } else {
  384. log.Println("SSH tunnel disabled by server")
  385. d.sshTunnel.Stop()
  386. }
  387. }
  388. // Update Dashboard tunnel config
  389. dashboardTunnelCfg := &TunnelConfig{
  390. Enabled: d.cfg.DashboardTunnel.Enabled,
  391. Server: d.cfg.DashboardTunnel.Server,
  392. Port: d.cfg.DashboardTunnel.Port,
  393. User: d.cfg.DashboardTunnel.User,
  394. RemotePort: d.cfg.DashboardTunnel.RemotePort,
  395. KeepaliveInterval: d.cfg.DashboardTunnel.KeepaliveInterval,
  396. ReconnectDelay: d.cfg.DashboardTunnel.ReconnectDelay,
  397. }
  398. d.dashboardTunnel.UpdateConfig(dashboardTunnelCfg)
  399. if dashboardTunnelChanged {
  400. if d.cfg.DashboardTunnel.Enabled && d.cfg.DashboardTunnel.RemotePort != 0 {
  401. log.Printf("Dashboard tunnel enabled by server (port %d)", d.cfg.DashboardTunnel.RemotePort)
  402. d.dashboardTunnel.Restart()
  403. } else {
  404. log.Println("Dashboard tunnel disabled by server")
  405. d.dashboardTunnel.Stop()
  406. }
  407. }
  408. // Update dashboard state
  409. if dashboardChanged {
  410. if d.cfg.Dashboard.Enabled {
  411. if !d.api.IsRunning() {
  412. log.Println("Dashboard enabled by server - starting HTTP API")
  413. go func() {
  414. if err := d.api.Start(d.httpAddr); err != nil && err != http.ErrServerClosed {
  415. log.Printf("HTTP server error: %v", err)
  416. }
  417. }()
  418. }
  419. } else {
  420. if d.api.IsRunning() {
  421. log.Println("Dashboard disabled by server - stopping HTTP API")
  422. d.api.Stop()
  423. }
  424. }
  425. }
  426. // Update network manager config - it will handle all network changes automatically
  427. // (eth0 settings are local-only, never from server)
  428. // (WiFi client and scanner coordination handled by Network Manager)
  429. d.netmgr.UpdateConfig(d.cfg)
  430. // Save updated config
  431. SaveConfig(d.configPath, d.cfg)
  432. }
  433. func (d *Daemon) subscribeLoop(name string, addr string) {
  434. for {
  435. select {
  436. case <-d.stopChan:
  437. return
  438. default:
  439. }
  440. // Only try to connect if the corresponding scanner is running
  441. scannerRunning := false
  442. if name == "ble" {
  443. scannerRunning = d.scanners.IsBLERunning()
  444. } else if name == "wifi" {
  445. scannerRunning = d.scanners.IsWiFiRunning()
  446. }
  447. if !scannerRunning {
  448. // Wait before checking again
  449. time.Sleep(5 * time.Second)
  450. continue
  451. }
  452. if err := d.runSubscriber(name, addr); err != nil {
  453. log.Printf("[%s] Subscriber error: %v, reconnecting...", name, err)
  454. time.Sleep(time.Second)
  455. }
  456. }
  457. }
  458. func (d *Daemon) runSubscriber(name string, addr string) error {
  459. ctx, cancel := context.WithCancel(context.Background())
  460. defer cancel()
  461. sub := zmq4.NewSub(ctx)
  462. defer sub.Close()
  463. // Subscribe to all topics for this type
  464. if err := sub.SetOption(zmq4.OptionSubscribe, name+"."); err != nil {
  465. return err
  466. }
  467. if err := sub.Dial(addr); err != nil {
  468. return err
  469. }
  470. log.Printf("[%s] Connected to %s", name, addr)
  471. // Monitor stop channel in goroutine
  472. go func() {
  473. <-d.stopChan
  474. cancel()
  475. }()
  476. for {
  477. msg, err := sub.Recv()
  478. if err != nil {
  479. return err
  480. }
  481. // Message is in first frame
  482. data := string(msg.Frames[0])
  483. // Parse message: "topic JSON"
  484. parts := strings.SplitN(data, " ", 2)
  485. if len(parts) != 2 {
  486. continue
  487. }
  488. var event interface{}
  489. if err := json.Unmarshal([]byte(parts[1]), &event); err != nil {
  490. continue
  491. }
  492. d.mu.Lock()
  493. if name == "ble" {
  494. d.bleEvents = append(d.bleEvents, event)
  495. if d.cfg.Debug {
  496. log.Printf("[%s] Received event, queue size: %d", name, len(d.bleEvents))
  497. }
  498. // Add to API for WebSocket broadcast
  499. if d.api != nil {
  500. d.api.AddBLEEvent(event)
  501. }
  502. } else {
  503. d.wifiEvents = append(d.wifiEvents, event)
  504. if d.cfg.Debug {
  505. log.Printf("[%s] Received event, queue size: %d", name, len(d.wifiEvents))
  506. }
  507. // Add to API for WebSocket broadcast
  508. if d.api != nil {
  509. d.api.AddWiFiEvent(event)
  510. }
  511. }
  512. d.mu.Unlock()
  513. }
  514. }
  515. func (d *Daemon) uploadLoop(name string, endpoint string, intervalMs int) {
  516. if intervalMs <= 0 {
  517. intervalMs = 2500
  518. }
  519. ticker := time.NewTicker(time.Duration(intervalMs) * time.Millisecond)
  520. defer ticker.Stop()
  521. for {
  522. select {
  523. case <-d.stopChan:
  524. return
  525. case <-ticker.C:
  526. }
  527. // Skip upload if not registered yet
  528. if d.state.DeviceToken == "" {
  529. continue
  530. }
  531. d.mu.Lock()
  532. var events []interface{}
  533. if name == "ble" {
  534. events = d.bleEvents
  535. d.bleEvents = nil
  536. } else {
  537. events = d.wifiEvents
  538. d.wifiEvents = nil
  539. }
  540. d.mu.Unlock()
  541. if len(events) == 0 {
  542. continue
  543. }
  544. if d.cfg.Debug {
  545. log.Printf("[%s] Batch ready: %d events, uploading...", name, len(events))
  546. }
  547. batch := &EventBatch{
  548. DeviceID: d.state.DeviceID,
  549. Events: events,
  550. }
  551. if err := d.client.UploadEvents(endpoint, batch); err != nil {
  552. // Increment failure counter
  553. if name == "ble" {
  554. d.bleUploadFailures++
  555. } else {
  556. d.wifiUploadFailures++
  557. }
  558. // Log only every 6th failure (once per minute) to reduce spam when network is down
  559. failCount := d.bleUploadFailures
  560. if name != "ble" {
  561. failCount = d.wifiUploadFailures
  562. }
  563. if failCount == 1 || failCount%6 == 0 {
  564. log.Printf("[%s] Upload failed: %v, spooling %d events (failures: %d)", name, err, len(events), failCount)
  565. }
  566. if err := d.spooler.Save(batch, name); err != nil {
  567. log.Printf("[%s] Spool save failed: %v", name, err)
  568. }
  569. } else {
  570. // Reset failure counter on success
  571. if name == "ble" {
  572. d.bleUploadFailures = 0
  573. } else {
  574. d.wifiUploadFailures = 0
  575. }
  576. log.Printf("[%s] Uploaded %d events to server", name, len(events))
  577. }
  578. }
  579. }
  580. func (d *Daemon) spoolFlushLoop() {
  581. ticker := time.NewTicker(10 * time.Second)
  582. defer ticker.Stop()
  583. for {
  584. select {
  585. case <-d.stopChan:
  586. return
  587. case <-ticker.C:
  588. }
  589. if d.state.DeviceToken == "" {
  590. continue
  591. }
  592. // Try to flush one batch
  593. batch, err := d.spooler.PopOldest()
  594. if err != nil || batch == nil {
  595. continue
  596. }
  597. // Try to upload (guess endpoint from event types)
  598. endpoint := "/events"
  599. eventType := "unknown"
  600. if len(batch.Events) > 0 {
  601. if ev, ok := batch.Events[0].(map[string]interface{}); ok {
  602. if t, ok := ev["type"].(string); ok {
  603. if strings.HasPrefix(t, "wifi") {
  604. endpoint = "/wifi"
  605. eventType = "wifi"
  606. } else {
  607. endpoint = "/ble"
  608. eventType = "ble"
  609. }
  610. }
  611. }
  612. }
  613. if err := d.client.UploadEvents(endpoint, batch); err != nil {
  614. // Re-spool if upload failed
  615. d.spooler.Save(batch, "retry")
  616. } else {
  617. log.Printf("[spool] Flushed %d %s events to server", len(batch.Events), eventType)
  618. }
  619. }
  620. }
  621. // updateWiFiCredentials sends WiFi credentials to server (Cloud Mode only)
  622. func (d *Daemon) updateWiFiCredentials(ssid, psk string) error {
  623. return d.client.UpdateWiFiCredentials(ssid, psk)
  624. }
  625. // getDeviceID returns a device ID based on MAC address
  626. func getDeviceID() string {
  627. // Try wlan0 first, then eth0
  628. for _, iface := range []string{"wlan0", "eth0"} {
  629. if mac := getInterfaceMAC(iface); mac != "" {
  630. return mac
  631. }
  632. }
  633. // Fallback to hostname
  634. host, _ := os.Hostname()
  635. return host
  636. }
  637. func getInterfaceMAC(name string) string {
  638. iface, err := net.InterfaceByName(name)
  639. if err != nil {
  640. return ""
  641. }
  642. return iface.HardwareAddr.String()
  643. }
  644. func getInterfaceIP(name string) string {
  645. iface, err := net.InterfaceByName(name)
  646. if err != nil {
  647. return ""
  648. }
  649. addrs, err := iface.Addrs()
  650. if err != nil {
  651. return ""
  652. }
  653. for _, addr := range addrs {
  654. if ipnet, ok := addr.(*net.IPNet); ok && ipnet.IP.To4() != nil {
  655. // Return IP with CIDR notation (e.g., 192.168.5.244/24)
  656. ones, _ := ipnet.Mask.Size()
  657. return fmt.Sprintf("%s/%d", ipnet.IP.String(), ones)
  658. }
  659. }
  660. return ""
  661. }