longshot/protocol/request/
mod.rs

1mod app_control;
2mod monitor;
3mod profile;
4mod recipe;
5
6use super::{hardware_enums::*, machine_enum::*};
7pub use app_control::*;
8pub use monitor::*;
9pub use profile::*;
10pub use recipe::*;
11
12/// Implements the encode part of an encode/decode pair for a request or response.
13pub trait PartialEncode {
14    fn partial_encode(&self, out: &mut Vec<u8>);
15
16    fn encode(&self) -> Vec<u8> {
17        let mut v = vec![];
18        self.partial_encode(&mut v);
19        v
20    }
21}
22
23impl PartialEncode for u8 {
24    fn partial_encode(&self, out: &mut Vec<u8>) {
25        out.push(*self);
26    }
27}
28
29impl PartialEncode for u16 {
30    fn partial_encode(&self, out: &mut Vec<u8>) {
31        out.push((*self >> 8) as u8);
32        out.push(*self as u8);
33    }
34}
35
36impl<T: PartialEncode> PartialEncode for Vec<T> {
37    fn partial_encode(&self, out: &mut Vec<u8>) {
38        for t in self.iter() {
39            t.partial_encode(out);
40        }
41    }
42}
43
44impl<T: MachineEnumerable<T>> PartialEncode for &MachineEnum<T> {
45    fn partial_encode(&self, out: &mut Vec<u8>) {
46        out.push((**self).into())
47    }
48}
49
50/// Implements the decode part of an encode/decode pair for a request or response.
51pub trait PartialDecode<T> {
52    /// Partially decodes this type from a buffer, advancing the input slice to the next item.
53    fn partial_decode(input: &mut &[u8]) -> Option<T>;
54
55    /// Decode a buffer fully, returning the unparsed remainder if available
56    fn decode(mut input: &[u8]) -> (Option<T>, &[u8]) {
57        let ret = Self::partial_decode(&mut input);
58        (ret, input)
59    }
60}
61
62impl<T: PartialDecode<T>> PartialDecode<Vec<T>> for Vec<T> {
63    fn partial_decode(input: &mut &[u8]) -> Option<Self> {
64        let mut v = vec![];
65        while !input.is_empty() {
66            v.push(<T>::partial_decode(input)?);
67        }
68        Some(v)
69    }
70}
71
72impl<T: MachineEnumerable<T>> PartialDecode<MachineEnum<T>> for MachineEnum<T> {
73    fn partial_decode(input: &mut &[u8]) -> Option<Self> {
74        let (head, tail) = input.split_first()?;
75        *input = tail;
76        Some(MachineEnum::decode(*head))
77    }
78}
79
80impl PartialDecode<u8> for u8 {
81    fn partial_decode(input: &mut &[u8]) -> Option<u8> {
82        let (head, tail) = input.split_first()?;
83        *input = tail;
84        Some(*head)
85    }
86}
87
88impl PartialDecode<u16> for u16 {
89    fn partial_decode(input: &mut &[u8]) -> Option<u16> {
90        let a = <u8>::partial_decode(input)? as u16;
91        let b = <u8>::partial_decode(input)? as u16;
92        Some((a << 8) | b)
93    }
94}
95
96impl PartialDecode<u32> for u32 {
97    fn partial_decode(input: &mut &[u8]) -> Option<u32> {
98        let a = <u16>::partial_decode(input)? as u32;
99        let b = <u16>::partial_decode(input)? as u32;
100        Some((a << 16) | b)
101    }
102}
103
104macro_rules! packet_definition {
105    (
106        $(
107            $name:ident
108            ( $( $req_name:tt $req_type:ty ),* $(,)? )
109            =>
110            ( $( $resp_name:tt $resp_type:ty ),* $(,)? )
111        ),* $(,)? ) => {
112
113        /// A request sent from the host to device.
114        #[allow(dead_code)]
115        #[derive(Clone, Debug, Eq, PartialEq)]
116        pub enum Request {
117            $(
118                $name( $($req_type),* ),
119            )*
120        }
121
122        impl PartialEncode for Request {
123            fn partial_encode(&self, mut out: &mut Vec<u8>) {
124                match self {
125                    $(
126                        Self::$name(
127                            $(
128                                $req_name
129                            ),*
130                        ) => {
131                            out.push(EcamRequestId::$name as u8);
132                            if self.is_response_required() {
133                                out.push(0xf0);
134                            } else {
135                                out.push(0x0f);
136                            }
137                            $($req_name.partial_encode(&mut out); )*
138                        }
139                    )*
140                }
141            }
142        }
143
144        impl Request {
145            pub fn ecam_request_id(&self) -> EcamRequestId {
146                match self {
147                    $( Self::$name(..) => { EcamRequestId::$name } )*
148                }
149            }
150        }
151
152        /// A response sent from the device to the host.
153        #[allow(dead_code)]
154        #[derive(Clone, Debug, Eq, PartialEq)]
155        pub enum Response {
156            $(
157                $name ( $($resp_type),* ),
158            )*
159        }
160
161        impl Response {
162            pub fn ecam_request_id(&self) -> EcamRequestId {
163                match self {
164                    $( Self::$name(..) => { EcamRequestId::$name } )*
165                }
166            }
167        }
168
169        impl PartialDecode<Response> for Response {
170            fn partial_decode(input: &mut &[u8]) -> Option<Self> {
171                if input.len() < 2 {
172                    return None;
173                }
174                let id = EcamRequestId::try_from(input[0]);
175                if let Ok(id) = id {
176                    let _ = input[1];
177                    *input = &input[2..];
178                    match id {
179                        $(
180                            EcamRequestId::$name => {
181                                $(
182                                    let $resp_name = <$resp_type>::partial_decode(input)?;
183                                )*
184                                return Some(Self::$name(
185                                    $( $resp_name ),*
186                                ));
187                            }
188                        )*
189                    }
190                }
191                None
192            }
193        }
194    };
195}
196
197packet_definition!(
198    SetBtMode() => (),
199    MonitorV0() => (),
200    MonitorV1() => (),
201    MonitorV2() => (response MonitorV2Response),
202    BeverageDispensingMode(
203        recipe MachineEnum<EcamBeverageId>,
204        trigger MachineEnum<EcamOperationTrigger>,
205        ingredients Vec<RecipeInfo<u16>>,
206        mode MachineEnum<EcamBeverageTasteType>) => (unknown0 u8, unknown1 u8),
207    AppControl(request AppControl) => (),
208    ParameterRead(parameter u16, len u8) => (),
209    ParameterWrite() => (),
210    ParameterReadExt(parameter u16, len u8) => (parameter u16, data Vec<u8>),
211    StatisticsRead(parameter u16, len u8) => (data Vec<Statistic>),
212    Checksum() => (),
213    ProfileNameRead(start u8, end u8) => (names Vec<WideStringWithIcon>),
214    ProfileNameWrite() => (),
215    RecipeQuantityRead(profile u8, recipe MachineEnum<EcamBeverageId>)
216        => (profile u8, recipe MachineEnum<EcamBeverageId>, ingredients Vec<RecipeInfo<u16>>),
217    RecipePriorityRead() => (priorities Vec<u8>),
218    ProfileSelection() => (),
219    RecipeNameRead(start u8, end u8) => (names Vec<WideStringWithIcon>),
220    RecipeNameWrite() => (),
221    SetFavoriteBeverages(profile u8, recipies Vec<u8>) => (),
222    RecipeMinMaxSync(recipe MachineEnum<EcamBeverageId>) => (recipe MachineEnum<EcamBeverageId>, bounds Vec<RecipeMinMaxInfo>),
223    PinSet() => (),
224    BeanSystemSelect() => (),
225    BeanSystemRead() => (),
226    BeanSystemWrite() => (),
227    PinRead() => (),
228    SetTime() => (),
229);
230
231impl Request {
232    fn is_response_required(&self) -> bool {
233        !matches!(
234            self,
235            Request::AppControl(..)
236                | Request::MonitorV0()
237                | Request::MonitorV1()
238                | Request::MonitorV2()
239                | Request::StatisticsRead(..)
240        )
241    }
242}
243
244/// A statistic read from the device.
245#[derive(Copy, Clone, Debug, Eq, PartialEq)]
246pub struct Statistic {
247    pub stat: u16,
248    pub value: u32,
249}
250
251impl PartialDecode<Statistic> for Statistic {
252    fn partial_decode(input: &mut &[u8]) -> Option<Self> {
253        let stat = <u16>::partial_decode(input)?;
254        let value = <u32>::partial_decode(input)?;
255        Some(Statistic { stat, value })
256    }
257}
258
259#[cfg(test)]
260mod test {
261    use super::*;
262    use crate::protocol::*;
263    use rstest::*;
264
265    #[rstest]
266    #[case(&crate::protocol::test::RESPONSE_BREW_RECEIVED)]
267    #[case(&crate::protocol::test::RESPONSE_STATUS_CAPPUCCINO_MILK)]
268    #[case(&crate::protocol::test::RESPONSE_STATUS_READY_AFTER_CAPPUCCINO)]
269    #[case(&crate::protocol::test::RESPONSE_STATUS_CLEANING_AFTER_CAPPUCCINO)]
270    #[case(&crate::protocol::test::RESPONSE_STATUS_STANDBY_NO_ALARMS)]
271    #[case(&crate::protocol::test::RESPONSE_STATUS_STANDBY_NO_WATER_TANK)]
272    #[case(&crate::protocol::test::RESPONSE_STATUS_STANDBY_WATER_SPOUT)]
273    #[case(&crate::protocol::test::RESPONSE_STATUS_STANDBY_NO_COFFEE_CONTAINER)]
274    fn real_packets_decode_as_expected(#[case] bytes: &[u8]) {
275        let (packet, remainder) = Response::decode(unwrap_packet(bytes));
276        let packet = packet.expect("Expected to decode something");
277        assert_eq!(remainder, &[]);
278        // Not actually testing the decoding of these packets, but at least we can print it
279        println!("{:?}", packet);
280    }
281
282    #[test]
283    fn test_decode_monitor_packet() {
284        let buf = [117_u8, 15, 1, 5, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0];
285        let input = &mut buf.as_slice();
286        assert_eq!(
287            <Response>::partial_decode(input).expect("Failed to decode"),
288            Response::MonitorV2(MonitorV2Response {
289                state: EcamMachineState::ReadyOrDispensing.into(),
290                accessory: EcamAccessory::Water.into(),
291                progress: 0,
292                percentage: 0,
293                switches: SwitchSet::of(&[
294                    EcamMachineSwitch::WaterSpout,
295                    EcamMachineSwitch::MotorDown
296                ]),
297                alarms: SwitchSet::empty(),
298                ..Default::default()
299            })
300        );
301    }
302
303    #[test]
304    fn test_decode_monitor_packet_alarm() {
305        let buf = [117_u8, 15, 1, 69, 0, 1, 0, 7, 0, 0, 0, 0, 0, 0, 0];
306        let input = &mut buf.as_slice();
307        assert_eq!(
308            <Response>::partial_decode(input).expect("Failed to decode"),
309            Response::MonitorV2(MonitorV2Response {
310                state: EcamMachineState::ReadyOrDispensing.into(),
311                accessory: EcamAccessory::Water.into(),
312                progress: 0,
313                percentage: 0,
314                switches: SwitchSet::of(&[
315                    EcamMachineSwitch::WaterSpout,
316                    EcamMachineSwitch::MotorDown,
317                    EcamMachineSwitch::WaterLevelLow,
318                ]),
319                alarms: SwitchSet::of(&[EcamMachineAlarm::EmptyWaterTank]),
320                ..Default::default()
321            })
322        );
323    }
324
325    #[test]
326    fn test_decode_profile_packet() {
327        let buf = [
328            164_u8, 240, 0, 77, 0, 97, 0, 116, 0, 116, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0,
329            77, 0, 105, 0, 97, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 80, 0, 82, 0, 79, 0,
330            70, 0, 73, 0, 76, 0, 69, 0, 32, 0, 51, 0, 0, 3,
331        ];
332        let input = &mut buf.as_slice();
333        assert_eq!(
334            <Response>::partial_decode(input).expect("Failed to decode"),
335            Response::ProfileNameRead(vec![
336                WideStringWithIcon::new("Matt", 3),
337                WideStringWithIcon::new("Mia", 8),
338                WideStringWithIcon::new("PROFILE 3", 3)
339            ])
340        )
341    }
342
343    #[test]
344    fn test_brew_coffee() {
345        let recipe = vec![
346            RecipeInfo::new(EcamIngredients::Coffee, 103),
347            RecipeInfo::new(EcamIngredients::Taste, 2),
348            RecipeInfo::new(EcamIngredients::Temp, 0),
349        ];
350        assert_eq!(
351            Request::BeverageDispensingMode(
352                EcamBeverageId::RegularCoffee.into(),
353                EcamOperationTrigger::Start.into(),
354                recipe,
355                EcamBeverageTasteType::PrepareInversion.into()
356            )
357            .encode(),
358            vec![
359                0x83, 0xf0, 0x02, 0x01, 0x01, 0x00, 0x67, 0x02, 0x02, 0x00, 0x00, 0x06
360            ]
361        );
362    }
363}