lipl_display_common/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use serde::{Deserialize, Serialize};
4use std::convert::TryFrom;
5use std::str::FromStr;
6use uuid::{uuid, Uuid};
7
8mod error;
9
10/// Error type
11pub use error::Error;
12pub type Result<T> = std::result::Result<T, Error>;
13
14pub trait HandleMessage {
15    fn handle_message(&mut self, message: Message);
16}
17
18/// Uuid identifying the display service on the gatt peripheral
19pub const SERVICE_UUID: Uuid = uuid!("27a70fc8-dc38-40c7-80bc-359462e4b808");
20/// Local name used in advertising
21pub const LOCAL_NAME: &str = "lipl";
22/// Manufacturer id used in advertising
23pub const MANUFACTURER_ID: u16 = 0xffff;
24
25/// Uuid identifying the text characteristic on the gatt peripheral
26pub const CHARACTERISTIC_TEXT_UUID: Uuid = uuid!("04973569-c039-4ce9-ad96-861589a74f9e");
27/// Uuid identifying the status characteristic on the gatt peripheral
28pub const CHARACTERISTIC_STATUS_UUID: Uuid = uuid!("61a8cb7f-d4c1-49b7-a3cf-f2c69dbb7aeb");
29/// Uuid identifying the command characteristic on the gatt peripheral
30pub const CHARACTERISTIC_COMMAND_UUID: Uuid = uuid!("da35e0b2-7864-49e5-aa47-8050d1cc1484");
31
32pub const WAIT_MESSAGE: &str = "Even geduld a.u.b. ...";
33
34pub const MESSAGES: &[(&str, Command); 7] = &[
35    ("d", Command::Dark),
36    ("l", Command::Light),
37    ("+", Command::Increase),
38    ("-", Command::Decrease),
39    ("?", Command::Wait),
40    ("e", Command::Exit),
41    ("o", Command::Poweroff),
42];
43
44pub trait BackgroundThread {
45    fn stop(&mut self);
46}
47
48/// Received value on the display service as change for the screen
49#[derive(Clone, Deserialize, Debug, Eq, PartialEq, Serialize)]
50#[serde(rename_all = "lowercase")]
51pub enum Message {
52    Part(String),
53    Status(String),
54    Command(Command),
55}
56
57impl Message {
58    pub fn is_stop(&self) -> bool {
59        [
60            Message::Command(Command::Exit),
61            Message::Command(Command::Poweroff),
62        ]
63        .contains(self)
64    }
65}
66
67impl std::fmt::Display for Message {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(
70            f,
71            "{}",
72            match self {
73                Message::Part(text) => format!("Text: {}", text),
74                Message::Status(status) => format!("Status: {}", status),
75                Message::Command(command) => format!("Command: {}", command),
76            }
77        )
78    }
79}
80
81/// Received value from command characteristic
82#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
83pub enum Command {
84    Poweroff,
85    Exit,
86    Increase,
87    Decrease,
88    Light,
89    Dark,
90    Wait,
91}
92
93impl std::fmt::Display for Command {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(
96            f,
97            "{}",
98            MESSAGES.iter().find(|s| &s.1 == self).map(|s| s.0).unwrap()
99        )
100    }
101}
102
103impl FromStr for Command {
104    type Err = error::Error;
105    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
106        MESSAGES
107            .iter()
108            .find(|t| t.0 == s)
109            .map(|t| t.1.clone())
110            .ok_or(error::Error::GattCharaceristicValueParsing(
111                "Invalid command".to_owned(),
112            ))
113    }
114}
115
116impl TryFrom<(&str, Uuid)> for Message {
117    type Error = Error;
118    fn try_from(received: (&str, Uuid)) -> Result<Self> {
119        let uuid = received.1;
120        let s = received.0.to_owned();
121
122        if uuid == CHARACTERISTIC_TEXT_UUID {
123            return Ok(Message::Part(s));
124        }
125
126        if uuid == CHARACTERISTIC_STATUS_UUID {
127            return Ok(Message::Status(s));
128        }
129
130        if uuid == CHARACTERISTIC_COMMAND_UUID {
131            return s.parse::<Command>().map(Message::Command);
132        }
133
134        Err(Error::GattCharaceristicValueParsing(s))
135    }
136}
137
138/// Model holding all that is needed to draw a screen
139///
140#[derive(Clone, Serialize, Default)]
141pub struct LiplScreen {
142    pub text: String,
143    pub status: String,
144    pub dark: bool,
145    #[serde(rename = "fontSize")]
146    pub font_size: f32,
147}
148
149impl LiplScreen {
150    /// LiplScreen constructor
151    ///
152    /// # Example
153    ///
154    /// ```
155    /// use lipl_display_common::LiplScreen;
156    /// let screen = LiplScreen::new(true, 30.0);
157    /// # assert!(screen.dark);
158    /// # assert_eq!(screen.font_size, 30.0);
159    /// ```
160    pub fn new(dark: bool, initial_font_size: f32) -> Self {
161        Self {
162            dark,
163            font_size: initial_font_size,
164            ..Default::default()
165        }
166    }
167}
168
169impl HandleMessage for LiplScreen {
170    //! Create a new screen with an update applied
171    //!
172    //! # Example
173    //!
174    //! ```
175    //! use lipl_display_common::{Command, LiplScreen, HandleMessage, Message};
176    //! let mut screen = LiplScreen::new(true, 40.0);
177    //! assert!(screen.dark);
178    //! screen.handle_message(Message::Command(Command::Light));
179    //! assert!(!screen.dark);
180    //! ```
181    //!
182    fn handle_message(&mut self, message: Message) {
183        match message {
184            Message::Command(command) => match command {
185                Command::Dark => {
186                    self.dark = true;
187                }
188                Command::Light => {
189                    self.dark = false;
190                }
191                Command::Decrease => {
192                    self.font_size = (self.font_size - 1.0).max(2.0);
193                }
194                Command::Increase => {
195                    self.font_size = (self.font_size + 1.0).min(100.0);
196                }
197                Command::Wait => {
198                    self.text = String::new();
199                    WAIT_MESSAGE.clone_into(&mut self.status);
200                }
201                Command::Exit => {}
202                Command::Poweroff => {}
203            },
204            Message::Part(part) => {
205                self.text = part;
206            }
207            Message::Status(status) => {
208                self.status = status;
209            }
210        }
211    }
212}
213
214#[cfg(test)]
215mod test {
216    use super::{Command, MESSAGES};
217
218    #[test]
219    fn parse() {
220        for message in MESSAGES {
221            assert_eq!(message.0.parse::<Command>().unwrap(), message.1);
222        }
223
224        for message in MESSAGES {
225            assert_eq!(message.1.to_string(), message.0.to_string());
226        }
227    }
228}