1use std::collections::HashSet;
2use std::sync::Arc;
3use std::time::SystemTime;
4
5use arc_swap::ArcSwapOption;
6use tokio::sync::broadcast;
7use tokio::time::MissedTickBehavior;
8
9use crate::error::{Error, Result};
10use crate::event::{Event, EventKind, ThresholdDirection};
11use crate::session::{recompute_capture_rate, Session, WatcherEntry};
12use crate::stream::EventStream;
13use visual_cortex_capture::{Frame, FrameView, Rate, Region};
14use visual_cortex_vision::{
15 Detector, DetectorError, DetectorOutput, FrameDiff, OcrDetector, OcrEngine, TemplateMatcher,
16};
17
18const DEGRADED_THRESHOLD: u32 = 5;
19
20pub(crate) enum Mode {
21 Change,
22 EveryMatch,
23 Threshold(Box<dyn Fn(f64) -> bool + Send + 'static>),
24 Template { threshold: f32 },
25}
26
27pub struct WatcherBuilder<'s> {
29 session: &'s Session,
30 name: String,
31 region: Region,
32 rate: Rate,
33 frame_diff: bool,
34 gates: Vec<Box<dyn Detector>>,
35 detector: Option<Box<dyn Detector>>,
36 mode: Mode,
37 template_error: Option<Error>,
38}
39
40impl<'s> WatcherBuilder<'s> {
41 pub(crate) fn new(session: &'s Session, name: &str) -> Self {
42 Self {
43 session,
44 name: name.to_string(),
45 region: Region::Full,
46 rate: Rate::hz(1.0),
47 frame_diff: true,
48 gates: Vec::new(),
49 detector: None,
50 mode: Mode::Change,
51 template_error: None,
52 }
53 }
54
55 pub fn region(mut self, region: Region) -> Self {
56 self.region = region;
57 self
58 }
59
60 pub fn rate(mut self, rate: Rate) -> Self {
61 self.rate = rate;
62 self
63 }
64
65 pub fn frame_diff(mut self, enabled: bool) -> Self {
68 self.frame_diff = enabled;
69 self
70 }
71
72 pub fn gate(mut self, gate: impl Detector) -> Self {
75 self.gates.push(Box::new(gate));
76 self
77 }
78
79 pub fn detector(mut self, detector: impl Detector) -> Self {
80 self.detector = Some(Box::new(detector));
81 self
82 }
83
84 pub fn on_change(mut self) -> Self {
86 self.mode = Mode::Change;
87 self
88 }
89
90 pub fn on_every_match(mut self) -> Self {
92 self.mode = Mode::EveryMatch;
93 self
94 }
95
96 pub fn on_threshold(mut self, predicate: impl Fn(f64) -> bool + Send + 'static) -> Self {
98 self.mode = Mode::Threshold(Box::new(predicate));
99 self
100 }
101
102 pub fn template(mut self, png_bytes: &[u8], threshold: f32) -> Self {
107 match TemplateMatcher::from_png_bytes(png_bytes) {
108 Ok(matcher) => {
109 self.detector = Some(Box::new(matcher));
110 self.mode = Mode::Template { threshold };
111 }
112 Err(e) => {
113 self.template_error =
114 Some(Error::InvalidTemplate(self.name.clone(), e.to_string()));
115 }
116 }
117 self
118 }
119
120 pub fn ocr_text(mut self, engine: impl OcrEngine) -> Self {
125 self.detector = Some(Box::new(OcrDetector::text(engine)));
126 self
127 }
128
129 pub fn ocr(
133 mut self,
134 engine: impl OcrEngine,
135 pattern: impl Fn(&str) -> Option<f64> + Send + 'static,
136 ) -> Self {
137 self.detector = Some(Box::new(OcrDetector::number(engine, pattern)));
138 self
139 }
140
141 pub fn subscribe(self) -> Result<EventStream> {
143 if let Some(e) = self.template_error {
144 return Err(e);
145 }
146
147 let detector = self
148 .detector
149 .ok_or_else(|| Error::MissingDetector(self.name.clone()))?;
150 {
153 let mut watchers = self.session.inner.watchers.lock().unwrap();
154 if watchers.contains_key(&self.name) {
155 return Err(Error::DuplicateWatcher(self.name));
156 }
157 watchers.insert(
158 self.name.clone(),
159 WatcherEntry {
160 rate: self.rate,
161 task: None,
162 },
163 );
164 }
165
166 let rx = self.session.inner.events.subscribe();
167 let runtime = WatcherRuntime {
168 name: Arc::from(self.name.as_str()),
169 region: self.region,
170 rate: self.rate,
171 frame_diff: self.frame_diff,
172 gates: self.gates,
173 detector: Some(detector),
174 mode: self.mode,
175 };
176 let task = tokio::spawn(watcher_loop(
177 runtime,
178 self.session.inner.slot.clone(),
179 self.session.inner.events.clone(),
180 ));
181 self.session
182 .inner
183 .watchers
184 .lock()
185 .unwrap()
186 .get_mut(&self.name)
187 .expect("entry reserved above")
188 .task = Some(task);
189 recompute_capture_rate(&self.session.inner);
190 Ok(EventStream::new(rx, HashSet::from([self.name])))
191 }
192}
193
194struct WatcherRuntime {
195 name: Arc<str>,
196 region: Region,
197 rate: Rate,
198 frame_diff: bool,
199 gates: Vec<Box<dyn Detector>>,
200 detector: Option<Box<dyn Detector>>,
201 mode: Mode,
202}
203
204async fn watcher_loop(
205 mut w: WatcherRuntime,
206 slot: Arc<ArcSwapOption<Frame>>,
207 events: broadcast::Sender<Event>,
208) {
209 let mut interval = tokio::time::interval(w.rate.period());
210 interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
211 let mut diff_gate = w.frame_diff.then(FrameDiff::new);
212 let mut last_output: Option<DetectorOutput> = None;
213 let mut inside_threshold: Option<bool> = None;
214 let mut consecutive_errors: u32 = 0;
215
216 loop {
217 interval.tick().await;
218 let Some(frame) = slot.load_full() else {
219 continue;
220 };
221 let rect = match w.region.resolve(frame.width(), frame.height()) {
222 Ok(rect) => rect,
223 Err(e) => {
224 tracing::warn!(watcher = %w.name, "region does not fit frame: {e}");
225 continue;
226 }
227 };
228 let view = match frame.view(rect) {
229 Ok(view) => view,
230 Err(e) => {
231 tracing::warn!(watcher = %w.name, "crop failed: {e}");
232 continue;
233 }
234 };
235 if let Some(gate) = &mut diff_gate {
236 if matches!(gate.evaluate(&view), Ok(DetectorOutput::Bool(false))) {
237 continue;
238 }
239 }
240 if !gates_pass(&mut w.gates, &view, &w.name) {
241 continue;
242 }
243 let result = if w.detector.as_ref().is_some_and(|d| d.is_heavy()) {
244 let mut detector = w.detector.take().expect("detector present");
247 let frame = frame.clone();
248 match tokio::task::spawn_blocking(move || {
249 let out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
253 let view = frame.view(rect).expect("rect was validated for this frame");
254 detector.evaluate(&view)
255 }))
256 .unwrap_or_else(|_| Err(DetectorError::Other("detector panicked".to_string())));
257 (detector, out)
258 })
259 .await
260 {
261 Ok((detector, out)) => {
262 w.detector = Some(detector);
263 out
264 }
265 Err(join_err) => {
266 tracing::error!(watcher = %w.name, "blocking task failed: {join_err}");
272 send(
273 &events,
274 &w.name,
275 EventKind::WatcherDegraded {
276 consecutive_errors: consecutive_errors + 1,
277 },
278 );
279 return;
280 }
281 }
282 } else {
283 w.detector
284 .as_mut()
285 .expect("detector present")
286 .evaluate(&view)
287 };
288 match result {
289 Ok(output) => {
290 consecutive_errors = 0;
291 emit(
292 &w.mode,
293 &w.name,
294 &events,
295 output,
296 &mut last_output,
297 &mut inside_threshold,
298 );
299 }
300 Err(e) => {
301 consecutive_errors += 1;
302 tracing::warn!(
303 watcher = %w.name,
304 "detector error ({consecutive_errors} consecutive): {e}"
305 );
306 if consecutive_errors == DEGRADED_THRESHOLD {
307 send(
308 &events,
309 &w.name,
310 EventKind::WatcherDegraded { consecutive_errors },
311 );
312 }
313 }
314 }
315 }
316}
317
318fn gates_pass(gates: &mut [Box<dyn Detector>], view: &FrameView<'_>, name: &Arc<str>) -> bool {
319 for gate in gates.iter_mut() {
320 match gate.evaluate(view) {
321 Ok(DetectorOutput::Bool(true)) => {}
322 Ok(_) => return false, Err(e) => {
324 tracing::warn!(watcher = %name, "gate error, skipping tick: {e}");
325 return false;
326 }
327 }
328 }
329 true
330}
331
332fn send(events: &broadcast::Sender<Event>, name: &Arc<str>, kind: EventKind) {
333 let _ = events.send(Event {
334 watcher: name.clone(),
335 timestamp: SystemTime::now(),
336 kind,
337 });
338}
339
340fn emit(
341 mode: &Mode,
342 name: &Arc<str>,
343 events: &broadcast::Sender<Event>,
344 output: DetectorOutput,
345 last_output: &mut Option<DetectorOutput>,
346 inside_threshold: &mut Option<bool>,
347) {
348 match mode {
349 Mode::EveryMatch => {
350 let matched = !matches!(output, DetectorOutput::None | DetectorOutput::Bool(false));
351 if matched {
352 send(events, name, EventKind::Matched { output });
353 }
354 }
355 Mode::Change => {
356 if last_output.as_ref() != Some(&output) {
357 send(
358 events,
359 name,
360 EventKind::Changed {
361 old: last_output.clone(),
362 new: output.clone(),
363 },
364 );
365 }
366 *last_output = Some(output);
367 }
368 Mode::Threshold(predicate) => {
369 let Some(value) = output.as_number() else {
370 return;
371 };
372 let inside = predicate(value);
373 match *inside_threshold {
374 None if inside => send(
377 events,
378 name,
379 EventKind::ThresholdCrossed {
380 value,
381 direction: ThresholdDirection::Entered,
382 },
383 ),
384 Some(prev) if prev != inside => {
385 let direction = if inside {
386 ThresholdDirection::Entered
387 } else {
388 ThresholdDirection::Exited
389 };
390 send(
391 events,
392 name,
393 EventKind::ThresholdCrossed { value, direction },
394 );
395 }
396 _ => {}
397 }
398 *inside_threshold = Some(inside);
399 }
400 Mode::Template { threshold } => {
401 let Some(score) = output.as_number() else {
402 return;
403 };
404 let present = score >= *threshold as f64;
405 match *inside_threshold {
406 None if present => send(
408 events,
409 name,
410 EventKind::TemplateAppeared {
411 score: score as f32,
412 },
413 ),
414 Some(prev) if prev != present => {
415 if present {
416 send(
417 events,
418 name,
419 EventKind::TemplateAppeared {
420 score: score as f32,
421 },
422 );
423 } else {
424 send(events, name, EventKind::TemplateVanished);
425 }
426 }
427 _ => {}
428 }
429 *inside_threshold = Some(present);
430 }
431 }
432}