package devicecheck import ( "context" "database/sql" "encoding/json" "log" "strings" "sync" "time" "git.piskot.si/SeminarM2/lambdaiot-core/internal/mqtt" ) // Checker periodically pings devices via MQTT and updates their status // based on responses received on the subscribed topic. type Checker struct { DB *sql.DB MQTT *mqtt.Client Topic string Interval time.Duration Timeout time.Duration mu sync.Mutex waiters map[string]chan struct{} } // New constructs a Checker with sane defaults for interval and timeout when zero. func New(db *sql.DB, mq *mqtt.Client, topic string, interval, timeout time.Duration) *Checker { if interval == 0 { interval = time.Minute } if timeout == 0 { timeout = 10 * time.Second } if topic == "" { topic = "lambdaiot" } return &Checker{ DB: db, MQTT: mq, Topic: topic, Interval: interval, Timeout: timeout, waiters: make(map[string]chan struct{}), } } // Start launches the periodic checker until the context is canceled. func (c *Checker) Start(ctx context.Context) { if c != nil { log.Printf("devicecheck: starting (topic=%s interval=%s timeout=%s)", c.Topic, c.Interval, c.Timeout) } go c.loop(ctx) } // HandleMessage attempts to parse a device state/check response message and // signals any waiter for the given device. Expected JSON payload shape: // // { // "type": "device_check_response" | "device_state", // "device_id": "uuid", // "status": "ok" | "lost" | "pending" // } func (c *Checker) HandleMessage(payload []byte) { if c == nil { return } var msg struct { Type string `json:"type"` DeviceID string `json:"device_id"` Status string `json:"status"` } if err := json.Unmarshal(payload, &msg); err != nil { return } id := strings.ToLower(strings.TrimSpace(msg.DeviceID)) if id == "" { return } msgType := strings.ToLower(strings.TrimSpace(msg.Type)) if msgType != "device_check_response" && msgType != "device_state" && !strings.EqualFold(msg.Status, "ok") { return } // Notify waiter if present for valid response/state c.mu.Lock() ch := c.waiters[id] c.mu.Unlock() if ch != nil { select { case ch <- struct{}{}: default: } } // Update status to ok on explicit ok or check response/state _ = c.updateStatus(id, 1) // 1 = ok } func (c *Checker) loop(ctx context.Context) { ticker := time.NewTicker(c.Interval) defer ticker.Stop() // Run immediately c.runOnce() for { select { case <-ctx.Done(): return case <-ticker.C: c.runOnce() } } } func (c *Checker) runOnce() { if c == nil || c.DB == nil || c.MQTT == nil { return } rows, err := c.DB.Query("SELECT BIN_TO_UUID(id) FROM devices WHERE status_id NOT IN (3, 4)") // skip lost/disabled if err != nil { log.Printf("devicecheck: query devices failed: %v", err) return } defer rows.Close() var ids []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { log.Printf("devicecheck: scan id failed: %v", err) return } ids = append(ids, strings.ToLower(id)) } if err := rows.Err(); err != nil { log.Printf("devicecheck: rows error: %v", err) return } if len(ids) == 0 { log.Printf("devicecheck: no devices found to check") return } var wg sync.WaitGroup for _, id := range ids { wg.Add(1) go func(deviceID string) { defer wg.Done() c.checkDevice(deviceID) }(id) } wg.Wait() } func (c *Checker) checkDevice(deviceID string) { if c == nil || c.DB == nil || c.MQTT == nil { return } ch := make(chan struct{}, 1) c.mu.Lock() c.waiters[deviceID] = ch c.mu.Unlock() payload := struct { Type string `json:"type"` DeviceID string `json:"device_id"` RequestedAt string `json:"requested_at"` }{ Type: "device_check_request", DeviceID: deviceID, RequestedAt: time.Now().UTC().Format(time.RFC3339), } body, err := json.Marshal(payload) if err == nil { log.Printf("devicecheck: publish request for %s", deviceID) if err := c.MQTT.Publish(c.Topic, body); err != nil { log.Printf("devicecheck: publish failed for %s: %v", deviceID, err) } } if err := c.updateStatus(deviceID, 2); err != nil { // 2 = pending log.Printf("devicecheck: set pending failed for %s: %v", deviceID, err) } select { case <-ch: if err := c.updateStatus(deviceID, 1); err != nil { // ok log.Printf("devicecheck: set ok failed for %s: %v", deviceID, err) } case <-time.After(c.Interval): if err := c.updateStatus(deviceID, 3); err != nil { // lost log.Printf("devicecheck: set lost failed for %s: %v", deviceID, err) } } c.mu.Lock() delete(c.waiters, deviceID) c.mu.Unlock() } func (c *Checker) updateStatus(deviceID string, statusID int) error { if c == nil || c.DB == nil { return nil } log.Printf("devicecheck: update status request %s -> %d", deviceID, statusID) res, err := c.DB.Exec("UPDATE devices SET status_id = ?, updated_at = NOW(6) WHERE id = UUID_TO_BIN(?)", statusID, deviceID) if err != nil { log.Printf("devicecheck: update status failed for %s -> %d: %v", deviceID, statusID, err) return err } rows, err := res.RowsAffected() if err != nil { log.Printf("devicecheck: rows affected failed for %s -> %d: %v", deviceID, statusID, err) return err } if rows == 0 { log.Printf("devicecheck: no rows updated for %s -> %d", deviceID, statusID) } return nil }