DEV Community

Neil Gall
Neil Gall

Posted on

Pirrigator, part 5.

Integration testing my hardware

In my earlier posts I outlined my plan to automate watering the tomatoes in my greenhouse with a Raspberry Pi and Rust. The first few posts mostly outlined the hardware and my procrastination at writing proper Rust code for the first time. But we're now into May and the plants will be going in soon, so I really need this working pretty quickly. The good news is much of the core code concepts are in place.

Configuration

I stumbled across the config crate in my news feed one morning and it was exactly what I need. It deserialises from various sources and formats into a data structure, so I can keep the hardware configuration and detail out of the main code.

#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct Database {
    pub path: String
}

#[derive(Debug, Deserialize, PartialEq, Eq)]
pub struct Settings {
    pub database: Database,
    pub weather: Option<WeatherSensorSettings>,
    pub adc: Option<ADCSettings>,
    pub moisture: Vec<MoistureSensorSettings>,
    pub buttons: Vec<ButtonSettings>,
    pub valves: Vec<ValveSettings>
}

impl Settings {
    pub fn new() -> Result<Self, ConfigError> {
        let mut s = Config::new();

        s.merge(File::with_name("Settings"))?;
        s.merge(Environment::with_prefix("PIRRIGATOR"))?;

        s.try_into()
    }
}
Enter fullscreen mode Exit fullscreen mode

A central setup chunk of code takes the settings read from a file and constructs the various objects I need. So I can do various tests on my development laptop I made all the hardware modules optional so I can configure the system with just a database and web server.

impl Pirrigator {
    pub fn new(s: settings::Settings) -> Result<Pirrigator, Box<Error>> {
        let (tx, rx) = mpsc::channel();

        let weather = traverse(&s.weather, &|w|
            weather::WeatherSensor::new(&w, tx.clone())
        )?;

        let moisture = traverse(&s.adc, &|adc|
            moisture::MoistureSensor::new(&adc, &s.moisture, tx.clone())
        )?;

        let buttons = button::Buttons::new(&s.buttons, tx.clone())?;
        let valves = valve::Valves::new(&s.valves)?;

        let db = database::Database::new(Path::new(&s.database.path))?;

        let mut controller = Controller {
            database: db.clone(),
            weather,
            moisture,
            buttons,
            valves
        };

        let thread = spawn(move || controller.run(rx));

        return Ok(Pirrigator { 
            thread,
            database: db
        })
    }
Enter fullscreen mode Exit fullscreen mode

The traverse feels like the wrong way to do things but I'm still learning how the Option and Result combinators all work. I'll sort it out at some point. It's a standard traverse function like in Haskell:

fn traverse<T, U, E>(t: &Option<T>, f: &Fn(&T) -> Result<U, E>) -> Result<Option<U>, E> {
    match t {
        None => Ok(None),
        Some(t) => f(t).map(Some)
    }
}
Enter fullscreen mode Exit fullscreen mode

Sending data from sensors

As I mentioned in an earlier post my grand idea was to run each sensor on its own thread and post events to a controller which dispatches these events to the places they need to go. For now, I store sensor data in an sqlite database, and act on the button presses, which are a manual on/off for the water valve for now.

The sensor modules all follow a similar pattern. new() spawns a thread which mostly sleeps but wakes up periodically, reads data from the hardware and posts an event to the multi-producer-single-consumer channel.

impl WeatherSensor {
    pub fn new(settings: &WeatherSensorSettings, channel: Sender<Event>) -> Result<Self, Box<Error>> {
        let device = Bme280Device::new(&settings.device, settings.address)?;
        let period = Duration::from_secs(settings.update);
        let thread = spawn(move || { main(device, channel, period) });
        Ok(WeatherSensor { thread })
    }
}
Enter fullscreen mode Exit fullscreen mode

The local main() function for a sensor thread is just a polling loop:

fn main(mut device: Bme280Device, channel: Sender<Event>, period: Duration) {
    loop {
        match device.read() {
            Ok(data) => send_event(data, &channel),
            Err(e) => error(e)
        };
        sleep(period);
    }
}
Enter fullscreen mode Exit fullscreen mode

And send_event() just translates away from the hardware module's data structure, adds a timestamp, and sends it over the channel.

fn send_event(data: Bme280Data, channel: &Sender<Event>) {
    let event = WeatherEvent {
        timestamp: SystemTime::now(),
        temperature: data.temperature,
        humidity: data.humidity,
        pressure: data.pressure
    };

     match channel.send(Event::WeatherEvent(event)) {
        Ok(_) => {},
        Err(e) => error(e)
    };
}
Enter fullscreen mode Exit fullscreen mode

Receiving data from sensors

The controller's run() is the main dispatcher loop. Nothing too fancy.

        loop {
            let event = rx.recv()
                .expect("receive error");

            self.database.store_event(&event)
                .expect("database store error");

            match event {
                event::Event::ButtonEvent(b) => self.button_event(&b),
                _ => {}
            }
        }
Enter fullscreen mode Exit fullscreen mode

For the database I chose rusqlite. I don't need an ORM or anything - there are no relations in my data yet so I'm just doing SQL by hand. I do want the controller's receiver thread and web server handler threads to be able to access the database however, so there's an r2d2 connection pool in there. The Database abstraction provides methods like

    fn store_weather(&self, event: &weather::WeatherEvent) -> Result<(), Error> {
        self.conn().execute(
            "INSERT INTO weather (time, temperature, humidity, pressure) VALUES (?1, ?2, ?3, ?4)",
            &[&to_seconds(&event.timestamp) as &ToSql, &event.temperature, &event.humidity, &event.pressure]
        )?;
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Dates are always a nightmare so I'm just translating the time to 32-bit Unix seconds. If I'm still using this in 2038 I'll be amazed. I had to cross-compile the sqlite C library for ARMv6 but this turned out to be really simple - just download the autoconf package, configure with --target=arm-unknown-linux-eabihf, make, and copy the resulting libraries to the Rust deps directory. It looks like Cargo can be configured to do all this, but it's not my main concern right now.

Web Server

Primarily it's an automation system but I do want to be able to check in, and of course with all that collected data we want graphs! So the final component so far is a web server. I looked at a few Rust web frameworks. Rocket looks great, similar in feel to Flask but I feel the developers have made a gigantic mistake requiring the Rust nightly compiler. Back in the early days of Swift I had a terrible time with code unable to compile across releases. Maybe Rust's change management is better than Apple's was in the Swift 2 to 3 days, but the unstable language base meant an instant rejection for Rocket.

Some of the other web frameworks looked far too removed from anything I've used before. But Iron seemed simple to get started and quite extensible so that's my choice for now. Following this example I managed to build a middleware to plug my database abstraction into requests:

struct DbMiddleware {
    database: Database
}

impl typemap::Key for DbMiddleware {
    type Value = Database;
}

impl BeforeMiddleware for DbMiddleware {
    fn before(&self, req: &mut Request) -> IronResult<()> {
        req.extensions.insert::<DbMiddleware>(self.database.clone());
        Ok(())
    }
}

pub trait DbRequestExtension {
    fn get_database(&self) -> Database;
}

impl <'a, 'b>DbRequestExtension for Request<'a, 'b> {
    fn get_database(&self) -> Database {
        let database = self.extensions.get::<DbMiddleware>().unwrap();
        database.clone()
    }
}
Enter fullscreen mode Exit fullscreen mode

Thus in any request handler I can do req.get_database() and invoke methods on the database abstraction. The middleware also plugs in a JSON response handler and a logger. All useful things that you need in almost any project.

So far the web server has a /status route which just reports that the app is running, and a /weather route which reports the latest recorded weather conditions.

fn status(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((status::Ok, "running")))
}

fn weather(req: &mut Request) -> IronResult<Response> {
    let weather = req.get_database().get_latest_weather();

    let mut response = Response::new();
    match weather {
        Ok(w) => {
            response.set_mut(JsonResponse::json(w)).set_mut(status::Ok);
            Ok(response)
        }
        Err(e) => {
            let err = IronError::new(e, status::NotFound);
            Err(err)
        }
    }
}

pub fn run(database: Database) {
    let mut router = Router::new();
    router.get("/status", status, "status");
    router.get("/weather", weather, "weather");

    Iron::new(middleware::insert(router, database))
        .http("0.0.0.0:5000")
        .unwrap();
}
Enter fullscreen mode Exit fullscreen mode

Again I'm sure there are more succinct and idiomatic translations between error types but I'll just have to learn these as I go. I was emphatic in an earlier post that you just have to do what works when learning, and improve it as you go.

After all that, I can GET a JSON blob with the current greenhouse weather conditions! For fun I exposed the port on my router and threw together an Alexa skill to query the weather and read it out. We've had fun asking Alexa what the weather in the greenhouse is like.

Still to do

Loads has come together in the last week or two, but there's tons to do. There's still no automation of course - I'll need a final thread which runs a state machine and some logic deciding when the plants need water. I haven't really thought much about this yet, but the infrastructure now available allows it to be modular and gives me options.

I'll also need to expand the web server to expose more data suitable for drawing graphs - the last hour, day, week of weather and moisture data for example. Since Rust has a WebAssembly target I'm starting to wonder if I could even build a front-end in Rust?

There are other odds and ends - there's a camera module hanging off the Raspberry Pi, so I want to be able to at least serve up a still image every few hours. Remote override of the water might be an idea; if I'm away from home and everything looks a bit dry because the automation isn't behaving correctly I could at least water the plants manually.

The experience

The first Rust code I wrote was to read the hardware sensors and collect the data into structures. It felt like C for the most part. Since then I've moved up the stack to accessing a database and serving web content, and Rust no longer feels like C. I've had few ownership issues and have always had a notion of "who is responsible for this object" when programming, so I'm actually really enjoying making this explicit in Rust. No exceptions is great - I think they result in terrible code - but finding the right way to use Rust's tools like Option and Result together while keeping the code simple and clean is tricky. I feel I'm doing too many unwrap()s and not enough passing errors up the call stack. All part of the learning process, which is ultimately what this project is about. If you want to learn a language do something real in it.

The full code is on github, as ever.

Top comments (1)

Collapse
 
jeikabu profile image
jeikabu

Pretty cool to see it coming together! I've been keeping an eye on this because it blows away my own Pi projects. Make sure to share a pic of the "fruits of your labor". ;)

I share your sentiment regarding nightly rust. I've been reluctant to use it much but with so many other projects using it it must be mostly ok. Mostly.