Skip to content

Project report #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
- 🤑 Only needs a Raspberry Pi Zero (or others) and a 15$ display
- ⚙️ Widgets can be configured by the user via a web interface

<div class="page"/>

## 📚 Table of contents

- [⭐️ What WG Display can show you](#️-what-wg-display-can-show-you)
Expand All @@ -42,34 +44,45 @@
- [Building the project](#building-the-project)
- [👏 Writing your own widget](#-writing-your-own-widget)
- [📖 Documentation (rustdocs)](#-documentation-rustdocs)
- [🔮 Upcoming features](#-upcoming-features)
- [🧪 Testing](#-testing)
- [🔮 What comes next](#-what-comes-next)
- [🔒 Safety](#-safety)
- [♻️ Updating the dependencies](#️-updating-the-dependencies)
- [🦾 Developing on target](#-developing-on-target)

![WG Display image front](docs/images/wg_display.jpg)

---
<div class="page"/>

![Configuration dashboard](docs/images/dashboard.jpeg)
The web interface allows the users to configure system aspects like the background color used on the display or various configuration options of the different widgets.

## 🚀 Getting started

1. Download the latest [release](https://github.com/eliabieri/wg_display/releases)
- Raspberry Pi Zero 1 / Zero W / Zero WH -> wg-display-arm-unknown-linux-gnueabihf
- Raspberry Pi 2 / 3 / 4 / Zero 2 W -> wg-display-armv7-unknown-linux-gnueabihf
2. Copy the binary over to the target
3. Add the full path of the binary to the end of ~/.bashrc
This way, the binary is run at reboot.
1. Change the hostname of the target to wgdisplay
`sudo raspi-config` -> `Network Options` -> `Hostname`
2. Download the latest [release](https://github.com/eliabieri/wg_display/releases)
- Raspberry Pi Zero 1 / Zero W / Zero WH -> `wg-display-arm-unknown-linux-gnueabihf`
- Raspberry Pi 2 / 3 / 4 / Zero 2 W -> `wg-display-armv7-unknown-linux-gnueabihf`
3. Copy the binary over to the target
`scp wg-display-arm-unknown-linux-gnueabihf [email protected]:/home/pi`
4. Enable the binary to be run at reboot
`echo "/home/pi/wg-display-arm-unknown-linux-gnueabihf" >> ~/.bashrc`
5. Allow the binary to be bind to port 80
`sudo setcap CAP_NET_BIND_SERVICE=+eip /home/pi/wg-display-arm-unknown-linux-gnueabihf`
6. Reboot the target
The configuration dashboard should be available at [wgdisplay.local](http://wgdisplay.local)

<div class="page"/>

## 🛠️ Assembling the hardware

WG Display is best deployed on a Raspberry Pi and a cheap display hat.

```text
💡 Even a Raspberry PI Zero is sufficient!
The application is very ressource efficient and generally only utilizes around 3% CPU on a Raspberry PI 3B.
💡 Even a Raspberry PI Zero is sufficient!
The application is very ressource efficient
and generally only utilizes around 3% CPU on a Raspberry PI 3B.
```

Some displays that are tested to be good
Expand Down Expand Up @@ -128,7 +141,8 @@ make app_armv7
Then simply copy over the generated binary to the target and run it.

```text
💡 To run it at boot, simply add the path to the binary to the end of the ~/.bashrc file.
💡 To run it at boot, simply add the path
to the binary to the end of the ~/.bashrc file.
```

## 👏 Writing your own widget
Expand Down Expand Up @@ -158,10 +172,18 @@ This generates three separate documentations, one for each crate
[common](common/target/doc/common/index.html): ```common/target/doc/common/index.html```
[frontend](frontend/target/doc/frontend/index.html): ```frontend/target/doc/frontend/index.html```

## 🔮 Upcoming features
## 🧪 Testing

Widgets should provide unit tests for their functionality where adequate.
Asynchronous functions can be tested using the [tokio_test::block_on](https://docs.rs/tokio-test/latest/tokio_test/fn.block_on.html) function.

## 🔮 What comes next

- [ ] Allow user to configure WiFi credentials via web interface
- [ ] Starting the binary through systemd
- [ ] Implement an update mechanism
- [ ] Implement authencation for the web interface
- [ ] Dynamically loading widgets (currently, the widgets are part of the app crate)

## 🔒 Safety

Expand All @@ -186,7 +208,8 @@ set -e
# Note:
# - Set hostname of target to wgdisplay
# - Add public key to authorized_keys on target
# - Enable root ssh login: https://raspberrypi.stackexchange.com/questions/48056/how-to-login-as-root-remotely
# - Enable root ssh login:
# https://raspberrypi.stackexchange.com/questions/48056/how-to-login-as-root-remotely

make app_arm
ssh [email protected] "sudo /usr/bin/pkill -9 app || true"
Expand Down
14 changes: 14 additions & 0 deletions app/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ time-humanize = "0.1.3"
cursive = { version = "0.20.0", features = [
"termion-backend",
], default-features = false }
# Dashboard

# Server
time = { version = "0.3.17", features = ["serde-well-known"] }
urlencoding = "2.1.2"
rocket = { version = "0.5.0-rc.2", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
rust-embed = "6.4.2"

# Persistence
sled = "0.34.7"
lazy_static = "1.4.0"

# Testing
tokio-test = "0.4.2"
114 changes: 113 additions & 1 deletion app/src/renderer/widgets/public_transport.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ impl Widget for PublicTransport {
async fn update(&mut self, config: &WidgetConfiguration) {
let config = &config.public_transport_config;
if config.from.is_empty() || config.to.is_empty() {
self.content = "From and to need to be specified!".to_string();
self.content = "`from` and `to` need to be configured!".to_string();
return;
}

Expand Down Expand Up @@ -122,6 +122,11 @@ impl PublicTransport {
fn update_departure_string(&mut self, num_departures: usize) {
self.content = format!("{} -> {}", self.data.from.name, self.data.to.name);

if self.data.connections.is_empty() {
self.content += "\nNo departures";
return;
}

let connections = self
.data
.connections
Expand Down Expand Up @@ -157,3 +162,110 @@ impl PublicTransport {
HumanTime::from(departure_offset.unsigned_abs()).to_text_en(Accuracy::Rough, Tense::Future)
}
}

#[cfg(test)]
mod tests {
use common::models::{BaseWidgetConfig, PublicTransportConfig, WidgetConfiguration};

use super::*;

#[test]
fn test_formatting() {
let mut public_transport = PublicTransport::new();
public_transport.last_updated = Some(Instant::now());
public_transport.data = PublicTransportData {
connections: vec![
ConnectionData {
from: FromData {
departure: OffsetDateTime::now_utc() + Duration::from_secs(23 * 60 + 1),
},
},
ConnectionData {
from: FromData {
departure: OffsetDateTime::now_utc() + Duration::from_secs(120 * 60 + 1),
},
},
],
from: FromMetaData {
name: "Bern".to_string(),
},
to: ToMetaData {
name: "Basel".to_string(),
},
};

let config = WidgetConfiguration {
public_transport_config: PublicTransportConfig {
base_config: BaseWidgetConfig { enabled: true },
from: "Bern".to_string(),
to: "Basel".to_string(),
num_connections_to_show: 2,
},
..Default::default()
};
tokio_test::block_on(public_transport.update(&config));

assert!(public_transport.content.contains("Bern -> Basel"));
assert!(public_transport.content.contains("\nin 23 minutes"));
assert!(public_transport.content.contains("\nin 2 hours"));
}

#[test]
fn test_formatting_with_no_departures() {
let mut public_transport = PublicTransport::new();
public_transport.last_updated = Some(Instant::now());
public_transport.data = PublicTransportData {
connections: vec![],
from: FromMetaData {
name: "Bern".to_string(),
},
to: ToMetaData {
name: "Basel".to_string(),
},
};

let config = WidgetConfiguration {
public_transport_config: PublicTransportConfig {
base_config: BaseWidgetConfig { enabled: true },
from: "Bern".to_string(),
to: "Basel".to_string(),
num_connections_to_show: 2,
},
..Default::default()
};
tokio_test::block_on(public_transport.update(&config));

assert_eq!(public_transport.content, "Bern -> Basel\nNo departures");
}

#[test]
fn test_from_to_not_configured() {
let mut public_transport = PublicTransport::new();
public_transport.last_updated = Some(Instant::now());
public_transport.data = PublicTransportData {
connections: vec![],
from: FromMetaData {
name: "Bern".to_string(),
},
to: ToMetaData {
name: "Basel".to_string(),
},
};

let config = WidgetConfiguration {
public_transport_config: PublicTransportConfig {
base_config: BaseWidgetConfig { enabled: true },
from: "".to_string(),
to: "".to_string(),
num_connections_to_show: 2,
},
..Default::default()
};
tokio_test::block_on(public_transport.update(&config));

assert_eq!(
public_transport.content,
"`from` and `to` need to be configured!"
);
}
}
Binary file added declaration_of_authorship.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading