Skip to main content

edge_core/
intent.rs

1//! Input primitives and service intents exchanged by the routing engine.
2
3use serde::{Deserialize, Serialize};
4
5/// Physical input from a device. Device-agnostic: a Nuimo rotate and a
6/// dial rotate both produce `Rotate { delta }`.
7#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub enum InputPrimitive {
9    Rotate {
10        delta: f64,
11    },
12    Press,
13    Release,
14    LongPress,
15    Swipe {
16        direction: Direction,
17    },
18    Slide {
19        value: f64,
20    },
21    Hover {
22        proximity: f64,
23    },
24    Touch {
25        area: TouchArea,
26    },
27    LongTouch {
28        area: TouchArea,
29    },
30    KeyPress {
31        key: u32,
32    },
33    /// Numbered button press from a multi-button controller (Hue Tap Dial
34    /// has 1..=4). Wire format: `"button_<id>"` (e.g. `button_1`).
35    Button {
36        id: u8,
37    },
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum Direction {
43    Up,
44    Down,
45    Left,
46    Right,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum TouchArea {
52    Top,
53    Bottom,
54    Left,
55    Right,
56}
57
58/// Service-level intent produced by the routing engine. Adapters translate
59/// this into their service's native command.
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
61#[serde(tag = "type", rename_all = "snake_case")]
62pub enum Intent {
63    Play,
64    Pause,
65    PlayPause,
66    Stop,
67    Next,
68    Previous,
69    VolumeChange { delta: f64 },
70    VolumeSet { value: f64 },
71    Mute,
72    Unmute,
73    SeekRelative { seconds: f64 },
74    SeekAbsolute { seconds: f64 },
75    BrightnessChange { delta: f64 },
76    BrightnessSet { value: f64 },
77    ColorTemperatureChange { delta: f64 },
78    PowerToggle,
79    PowerOn,
80    PowerOff,
81}
82
83impl InputPrimitive {
84    /// Match this primitive against a wire-format route input string
85    /// (e.g. "rotate", "press", "swipe_right", "touch_top").
86    pub fn matches_route(&self, route_input: &str) -> bool {
87        match (self, route_input) {
88            (InputPrimitive::Rotate { .. }, "rotate") => true,
89            (InputPrimitive::Press, "press") => true,
90            (InputPrimitive::Release, "release") => true,
91            (InputPrimitive::LongPress, "long_press") => true,
92            (InputPrimitive::Slide { .. }, "slide") => true,
93            (InputPrimitive::Hover { .. }, "hover") => true,
94            (InputPrimitive::Swipe { direction }, s) => matches!(
95                (direction, s),
96                (Direction::Up, "swipe_up")
97                    | (Direction::Down, "swipe_down")
98                    | (Direction::Left, "swipe_left")
99                    | (Direction::Right, "swipe_right")
100            ),
101            (InputPrimitive::Touch { area }, s) => matches!(
102                (area, s),
103                (TouchArea::Top, "touch_top")
104                    | (TouchArea::Bottom, "touch_bottom")
105                    | (TouchArea::Left, "touch_left")
106                    | (TouchArea::Right, "touch_right")
107            ),
108            (InputPrimitive::LongTouch { area }, s) => matches!(
109                (area, s),
110                (TouchArea::Top, "long_touch_top")
111                    | (TouchArea::Bottom, "long_touch_bottom")
112                    | (TouchArea::Left, "long_touch_left")
113                    | (TouchArea::Right, "long_touch_right")
114            ),
115            (InputPrimitive::Button { id }, s) => s
116                .strip_prefix("button_")
117                .and_then(|n| n.parse::<u8>().ok())
118                .is_some_and(|parsed| parsed == *id),
119            _ => false,
120        }
121    }
122
123    /// Extract the continuous value for a rotate/slide, if applicable.
124    pub fn continuous_value(&self) -> Option<f64> {
125        match self {
126            InputPrimitive::Rotate { delta } | InputPrimitive::Slide { value: delta } => {
127                Some(*delta)
128            }
129            _ => None,
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn rotate_matches_rotate_route() {
140        let r = InputPrimitive::Rotate { delta: 0.03 };
141        assert!(r.matches_route("rotate"));
142        assert!(!r.matches_route("press"));
143    }
144
145    #[test]
146    fn swipe_direction_matters() {
147        let s = InputPrimitive::Swipe {
148            direction: Direction::Right,
149        };
150        assert!(s.matches_route("swipe_right"));
151        assert!(!s.matches_route("swipe_left"));
152        assert!(!s.matches_route("swipe_up"));
153    }
154
155    #[test]
156    fn button_id_matches_numbered_route() {
157        let b = InputPrimitive::Button { id: 1 };
158        assert!(b.matches_route("button_1"));
159        assert!(!b.matches_route("button_2"));
160        assert!(!b.matches_route("button_"));
161        assert!(!b.matches_route("press"));
162    }
163
164    #[test]
165    fn button_route_rejects_non_numeric_suffix() {
166        let b = InputPrimitive::Button { id: 3 };
167        assert!(!b.matches_route("button_x"));
168        assert!(!b.matches_route("button_3a"));
169        assert!(b.matches_route("button_3"));
170    }
171}