raspberry_web/
handlers.rs

1use crate::models;
2use crate::utilities::get_allowed_states;
3use actix::{Actor, Handler, Message, SyncContext};
4use actix_web::{error, Error as actixError};
5use chrono::Local;
6use diesel::prelude::*;
7use diesel::r2d2::{ConnectionManager, Pool};
8
9//use utilities::get_allowed_states;
10
11/// This is db executor actor. We are going to run 3 of them in parallel.
12pub struct DbExecutor(pub Pool<ConnectionManager<SqliteConnection>>);
13
14impl Actor for DbExecutor {
15    type Context = SyncContext<Self>;
16}
17
18pub struct GpioId {
19    pub gpio_id: i32,
20}
21
22impl Message for GpioId {
23    type Result = Result<models::Gpio, actixError>;
24}
25
26pub struct CheckGpioLevel {
27    pub gpio_id: i32,
28    pub gpio_level: String,
29}
30
31impl Message for CheckGpioLevel {
32    type Result = Result<models::Gpio, actixError>;
33}
34
35pub struct SetGpioLevel {
36    pub gpio_id: i32,
37    pub gpio_level: String,
38}
39
40impl Message for SetGpioLevel {
41    type Result = Result<models::Gpio, actixError>;
42}
43
44impl Handler<GpioId> for DbExecutor {
45    type Result = Result<models::Gpio, actixError>;
46
47    fn handle(&mut self, msg: GpioId, _: &mut Self::Context) -> Self::Result {
48        use crate::schema::gpio_state::dsl::*;
49
50        let connection = &self
51            .0
52            .get()
53            .map_err(|_| error::ErrorInternalServerError("Error obtaining database connection"))?;
54
55        let mut gpio_vec = gpio_state
56            .filter(gpio_id.eq(msg.gpio_id))
57            .load::<models::Gpio>(connection)
58            .map_err(|_| error::ErrorInternalServerError("Error loading from database"))?;
59
60        gpio_vec.pop().ok_or({
61            // GPIO not set up in database
62            error::ErrorNotFound(format!(
63                "raspberry-web has not been configured to work with GPIO #{}",
64                msg.gpio_id
65            ))
66        })
67    }
68}
69
70impl Handler<CheckGpioLevel> for DbExecutor {
71    type Result = Result<models::Gpio, actixError>;
72
73    fn handle(&mut self, msg: CheckGpioLevel, _: &mut Self::Context) -> Self::Result {
74        let required_gpio_mode = "output";
75        use crate::schema::gpio_state::dsl::*;
76        let connection = &self
77            .0
78            .get()
79            .map_err(|_| error::ErrorInternalServerError("Error obtaining database connection"))?;
80
81        // 1. Load Vec<Gpio> from database
82        let gpio_before = gpio_state
83            .filter(gpio_id.eq(msg.gpio_id))
84            .load::<models::Gpio>(connection)
85            .map_err(|_| error::ErrorInternalServerError("Error loading from database"))?
86            .pop()
87            .ok_or({
88                error::ErrorNotFound(format!(
89                    "raspberry-web has not been configured to work with GPIO #{}",
90                    msg.gpio_id
91                ))
92            })?;
93
94        // 2. Check if the GPIO is in use
95        let bool_in_use = gpio_before.in_use == 1;
96        if !bool_in_use {
97            info!("GPIO #{} is not in use.", msg.gpio_id);
98            return Err(error::ErrorForbidden(format!(
99                "GPIO #{} is not in use.",
100                msg.gpio_id
101            )));
102        }
103
104        // 3. check if gpio_mode = 'output'
105        let none_replacement = "".to_string();
106        // https://stackoverflow.com/questions/22282117/how-do-i-borrow-a-reference-to-what-is-inside-an-optiont
107        let gpio_mode_before = gpio_before.gpio_mode.as_ref().unwrap_or(&none_replacement);
108        if gpio_mode_before != required_gpio_mode {
109            let message = format!(
110                "Level '{}' is not allowed for mode '{}'",
111                msg.gpio_level, gpio_mode_before
112            );
113            info!("{}", message);
114            return Err(error::ErrorForbidden(message));
115        }
116
117        // 4. Check if desired level 'msg.gpio_level' is allowed
118        let desired_level = msg.gpio_level.to_lowercase();
119        let state_map = get_allowed_states(connection, "level")
120            .map_err(|_| error::ErrorInternalServerError("Error loading from database"))?;
121
122        let allowed = state_map
123            .get::<str>(&desired_level)
124            .ok_or(error::ErrorNotFound(format!(
125                "Level '{}' is not a recognized GPIO state'",
126                desired_level
127            )))?;
128        if !allowed {
129            info!(
130                "Level '{}' is not an allowed state for GPIO #{}",
131                desired_level, msg.gpio_id
132            );
133            Err(error::ErrorForbidden("State not allowed"))?
134        }
135
136        Ok(gpio_before)
137    }
138}
139
140impl Handler<SetGpioLevel> for DbExecutor {
141    type Result = Result<models::Gpio, actixError>;
142
143    fn handle(&mut self, msg: SetGpioLevel, _: &mut Self::Context) -> Self::Result {
144        use crate::schema::gpio_state::dsl::*;
145        let connection = &self
146            .0
147            .get()
148            .map_err(|_| error::ErrorInternalServerError("Error obtaining database connection"))?;
149
150        // 5. Change the level
151        let target = gpio_state.filter(gpio_id.eq(msg.gpio_id));
152
153        let _result = diesel::update(target)
154            .set((
155                last_change.eq(Local::now().naive_local().to_string()),
156                gpio_level.eq(msg.gpio_level.to_lowercase()),
157            ))
158            .execute(connection);
159
160        // 6. Return Gpio state after update
161        let mut gpio_vec_after = gpio_state
162            .filter(gpio_id.eq(msg.gpio_id))
163            .load::<models::Gpio>(connection)
164            .map_err(|_| error::ErrorInternalServerError("Error loading from database"))?;
165
166        gpio_vec_after.pop().ok_or(
167            // At this point we msg.gpio.id is in db
168            error::ErrorInternalServerError("Could not connect to database"),
169        )
170    }
171}