package main import ( "sort" "time" "sdr-wideband-suite/internal/pipeline" ) type decisionQueueStats struct { RecordQueued int `json:"record_queued"` DecodeQueued int `json:"decode_queued"` RecordSelected int `json:"record_selected"` DecodeSelected int `json:"decode_selected"` RecordOldestS float64 `json:"record_oldest_sec"` DecodeOldestS float64 `json:"decode_oldest_sec"` } type queuedDecision struct { ID int64 SNRDb float64 FirstSeen time.Time LastSeen time.Time } type decisionQueues struct { record map[int64]*queuedDecision decode map[int64]*queuedDecision } func newDecisionQueues() *decisionQueues { return &decisionQueues{record: map[int64]*queuedDecision{}, decode: map[int64]*queuedDecision{}} } func (dq *decisionQueues) Apply(decisions []pipeline.SignalDecision, maxRecord int, maxDecode int, now time.Time) decisionQueueStats { if dq == nil { return decisionQueueStats{} } recSeen := map[int64]bool{} decSeen := map[int64]bool{} for i := range decisions { id := decisions[i].Candidate.ID if id == 0 { continue } if decisions[i].ShouldRecord { qd := dq.record[id] if qd == nil { qd = &queuedDecision{ID: id, FirstSeen: now} dq.record[id] = qd } qd.SNRDb = decisions[i].Candidate.SNRDb qd.LastSeen = now recSeen[id] = true } if decisions[i].ShouldAutoDecode { qd := dq.decode[id] if qd == nil { qd = &queuedDecision{ID: id, FirstSeen: now} dq.decode[id] = qd } qd.SNRDb = decisions[i].Candidate.SNRDb qd.LastSeen = now decSeen[id] = true } } for id := range dq.record { if !recSeen[id] { delete(dq.record, id) } } for id := range dq.decode { if !decSeen[id] { delete(dq.decode, id) } } recSelected := selectQueued(dq.record, maxRecord, now) decSelected := selectQueued(dq.decode, maxDecode, now) stats := decisionQueueStats{ RecordQueued: len(dq.record), DecodeQueued: len(dq.decode), RecordSelected: len(recSelected), DecodeSelected: len(decSelected), RecordOldestS: oldestAge(dq.record, now), DecodeOldestS: oldestAge(dq.decode, now), } for i := range decisions { id := decisions[i].Candidate.ID if decisions[i].ShouldRecord { if _, ok := recSelected[id]; !ok { decisions[i].ShouldRecord = false decisions[i].Reason = "queued: record budget" } } if decisions[i].ShouldAutoDecode { if _, ok := decSelected[id]; !ok { decisions[i].ShouldAutoDecode = false if decisions[i].Reason == "" { decisions[i].Reason = "queued: decode budget" } } } } return stats } func selectQueued(queue map[int64]*queuedDecision, max int, now time.Time) map[int64]struct{} { selected := map[int64]struct{}{} if len(queue) == 0 { return selected } type scored struct { id int64 score float64 } scoredList := make([]scored, 0, len(queue)) for id, qd := range queue { age := now.Sub(qd.FirstSeen).Seconds() boost := age / 2.0 if boost > 5 { boost = 5 } scoredList = append(scoredList, scored{id: id, score: qd.SNRDb + boost}) } sort.Slice(scoredList, func(i, j int) bool { return scoredList[i].score > scoredList[j].score }) limit := max if limit <= 0 || limit > len(scoredList) { limit = len(scoredList) } for i := 0; i < limit; i++ { selected[scoredList[i].id] = struct{}{} } return selected } func oldestAge(queue map[int64]*queuedDecision, now time.Time) float64 { oldest := 0.0 first := true for _, qd := range queue { age := now.Sub(qd.FirstSeen).Seconds() if first || age > oldest { oldest = age first = false } } if first { return 0 } return oldest }