Add peach-buttons and peach-oled

This commit is contained in:
mhfowler 2021-10-25 11:30:43 +02:00
parent d8803e9974
commit 9704a78149
22 changed files with 4249 additions and 0 deletions

View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

2
peach-buttons/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
**/*.rs.bk

View File

@ -0,0 +1,7 @@
language: rust
rust:
- nightly
before_script:
- rustup component add clippy
script:
- cargo clippy -- -D warnings

1460
peach-buttons/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

40
peach-buttons/Cargo.toml Normal file
View File

@ -0,0 +1,40 @@
[package]
name = "peach-buttons"
version = "0.1.3"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018"
description = "peach-buttons is a GPIO microservice for handling button presses, implementing a JSON-RPC server with Publish-Subscribe extension. Each button press results in a JSON-RPC request being sent over websockets to any subscribers. A button code for the pressed button is sent with the request to subscribers, allowing state-specific actions to be taken."
homepage = "https://opencollective.com/peachcloud"
repository = "https://github.com/peachcloud/peach-buttons"
readme = "README.md"
license = "AGPL-3.0-only"
publish = false
[package.metadata.deb]
depends = "$auto"
extended-description = """\
peach-buttons is a GPIO microservice for handling button presses, \
implementing a JSON-RPC server with Publish-Subscribe extention. \
Each button press results in a JSON-RPC request being sent over websockets \
to any subscribers."""
maintainer-scripts="debian"
systemd-units = { unit-name = "peach-buttons" }
assets = [
["target/release/peach-buttons", "usr/bin/", "755"],
["README.md", "usr/share/doc/peach-buttons/README", "644"],
]
[badges]
travis-ci = { repository = "peachcloud/peach-buttons", branch = "master" }
maintenance = { status = "actively-developed" }
[dependencies]
crossbeam-channel = "0.3"
env_logger = "0.6"
gpio-cdev = "0.2"
jsonrpc-core = "11"
jsonrpc-ws-server = "11"
jsonrpc-pubsub = "11"
jsonrpc-test = "11"
log = "0.4"
snafu = "0.4"

106
peach-buttons/README.md Normal file
View File

@ -0,0 +1,106 @@
# peach-buttons
[![Build Status](https://travis-ci.com/peachcloud/peach-buttons.svg?branch=master)](https://travis-ci.com/peachcloud/peach-buttons) ![Generic badge](https://img.shields.io/badge/version-0.1.3-<COLOR>.svg)
GPIO microservice module for handling button presses. `peach-buttons` implements a JSON-RPC server with [Publish-Subscribe extension](https://docs.rs/jsonrpc-pubsub/11.0.0/jsonrpc_pubsub/). Each button press results in a JSON-RPC request being sent over websockets to any subscribers. A button code for the pressed button is sent with the request to subscribers, allowing state-specific actions to be taken.
A subscriber implementation for this microservice can be found in the [peach-menu repo](https://github.com/peachcloud/peach-menu).
_Note: This module is a work-in-progress._
### Pin to Button to Button Code Mappings
```
4 => Center => 0,
27 => Left => 1,
23 => Right => 2,
17 => Up => 3,
22 => Down => 4,
5 => A => 5,
6 => B => 6
```
_Note: `peach-buttons` utilizes the GPIO character device ABI. This API, stabilized with Linux v4.4, deprecates the legacy sysfs interface to GPIOs that is planned to be removed from the upstream kernel after year 2020._
### Environment
The JSON-RPC WS server address and port can be configured with the `PEACH_BUTTONS_SERVER` environment variable:
`export PEACH_BUTTONS_SERVER=127.0.0.1:5000`
When not set, the value defaults to `127.0.0.1:5111`.
Logging is made availabe with `env_logger`:
`export RUST_LOG=info`
Other logging levels include `debug`, `warn` and `error`.
### Setup
Clone this repo:
`git clone https://github.com/peachcloud/peach-buttons.git`
Move into the repo and compile:
`cd peach-buttons`
`cargo build --release`
Run the binary with sudo:
`sudo ./target/release/peach-buttons`
### Debian Packaging
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-buttons` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
Install `cargo-deb`:
`cargo install cargo-deb`
Move into the repo:
`cd peach-buttons`
Build the package:
`cargo deb`
The output will be written to `target/debian/peach-buttons_0.1.0_arm64.deb` (or similar).
Build the package (aarch64):
`cargo deb --target aarch64-unknown-linux-gnu`
Install the package as follows:
`sudo dpkg -i target/debian/peach-buttons_0.1.0_arm64.deb`
The service will be automatically enabled and started.
Uninstall the service:
`sudo apt-get remove peach-buttons`
Remove configuration files (not removed with `apt-get remove`):
`sudo apt-get purge peach-buttons`
### Testing Subscription
Request:
`{"id":1,"jsonrpc":"2.0","method":"subscribe_buttons"}`
Response:
`{"jsonrpc":"2.0","result":1,"id":1}`
Event:
`{"jsonrpc":"2.0","method":"button_press","params":[0]}`
### Licensing
AGPL-3.0

View File

@ -0,0 +1,27 @@
[Unit]
Description=GPIO microservice for handling button presses. Implements a JSON-RPC server with Publish-Subscribe extension.
[Service]
Type=simple
User=peach-buttons
Group=gpio-user
Environment="RUST_LOG=error"
ExecStart=/usr/bin/peach-buttons
Restart=always
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYS_BOOT CAP_SYS_TIME CAP_KILL CAP_WAKE_ALARM CAP_LINUX_IMMUTABLE CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_NICE CAP_SYS_RESOURCE CAP_RAWIO CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_* CAP_FOWNER CAP_IPC_OWNER CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_AUDIT_*
InaccessibleDirectories=/home
LockPersonality=yes
NoNewPrivileges=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectSystem=yes
ReadOnlyDirectories=/var
RestrictAddressFamilies=~AF_INET6 AF_UNIX
SystemCallFilter=~@reboot @clock @debug @module @mount @swap @resources @privileged
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,25 @@
use std::{error, str};
use jsonrpc_core::{types::error::Error, ErrorCode};
use snafu::Snafu;
pub type BoxError = Box<dyn error::Error>;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum ButtonError {
#[snafu(display("Invalid parameters. Subscription rejected"))]
RejectSubscription,
}
impl From<ButtonError> for Error {
fn from(err: ButtonError) -> Self {
match &err {
ButtonError::RejectSubscription => Error {
code: ErrorCode::ParseError,
message: "Invalid parameters. Subscription request rejected".to_string(),
data: None,
},
}
}
}

View File

@ -0,0 +1,63 @@
use std::{cell::Cell, process, thread, time::Duration};
use crossbeam_channel::{tick, Sender};
use gpio_cdev::{Chip, LineRequestFlags};
use log::{debug, error, info};
// initialize gpio pin and poll for state
// send button code to "subscribe_buttons" rpc method for sink notification
pub fn interrupt_handler(pin: u32, button_code: u8, button_name: String, s: Sender<u8>) {
thread::spawn(move || {
debug!("Creating handle for GPIO chip.");
let mut chip = Chip::new("/dev/gpiochip0").unwrap_or_else(|err| {
error!("Failed to create handle for GPIO chip: {}", err);
process::exit(1);
});
debug!("Creating handle for GPIO line at given pin.");
let input = chip.get_line(pin).unwrap_or_else(|err| {
error!(
"Failed to create handle for GPIO line at pin {}: {}",
pin, err
);
process::exit(1);
});
let line_handle = input
.request(LineRequestFlags::INPUT, 0, &button_name)
.unwrap_or_else(|err| {
error!("Failed to gain kernel access for pin {}: {}", pin, err);
process::exit(1);
});
let ticker = tick(Duration::from_millis(2));
let mut counter = Cell::new(0);
let mut switch = Cell::new(0);
info!(
"Initating polling loop for {} button on pin {}",
button_name, pin
);
loop {
ticker.recv().unwrap();
let value = line_handle
.get_value()
.expect("Failed to get current state of this line from the kernel");
match value {
0 => counter.set(0),
1 => *counter.get_mut() += 1,
_ => (),
}
if counter.get() == 10 {
if switch.get() == 0 {
*switch.get_mut() += 1
} else {
debug!("Sending button code: {}", button_code);
s.send(button_code).unwrap_or_else(|err| {
error!("Failed to send button_code to publisher: {}", err);
});
}
}
}
});
}

119
peach-buttons/src/lib.rs Normal file
View File

@ -0,0 +1,119 @@
mod error;
mod interrupt;
use std::{env, result::Result, sync::Arc, thread};
use crossbeam_channel::bounded;
use jsonrpc_core::futures::Future;
use jsonrpc_core::*;
use jsonrpc_pubsub::{PubSubHandler, Session, Subscriber, SubscriptionId};
#[allow(unused_imports)]
use jsonrpc_test as test;
use jsonrpc_ws_server::{RequestContext, ServerBuilder};
use log::{debug, error, info, warn};
use crate::error::{BoxError, ButtonError::RejectSubscription};
use crate::interrupt::*;
pub fn run() -> Result<(), BoxError> {
info!("Starting up.");
debug!("Creating channel for message passing.");
let (s, r) = bounded(0);
let pin = vec![4, 27, 23, 17, 22, 5, 6];
let code = vec![0, 1, 2, 3, 4, 5, 6];
let name = vec!["center", "left", "right", "up", "down", "#5", "#6"];
debug!("Setting up interrupt handlers.");
for i in 0..7 {
interrupt_handler(pin[i], code[i], name[i].to_string(), s.clone());
}
debug!("Creating pub-sub handler.");
let mut io = PubSubHandler::new(MetaIoHandler::default());
io.add_subscription(
"button_press",
(
"subscribe_buttons",
move |params: Params, _, subscriber: Subscriber| {
debug!("Received subscription request.");
if params != Params::None {
subscriber
.reject(Error::from(RejectSubscription))
.unwrap_or_else(|_| {
error!("Failed to send rejection error for subscription request.");
});
return;
}
let r1 = r.clone();
thread::spawn(move || {
let sink = subscriber
.assign_id_async(SubscriptionId::Number(1))
.wait()
.unwrap();
info!("Listening for button code from gpio events.");
loop {
let button_code: u8 = r1.recv().unwrap();
info!("Received button code: {}.", button_code);
match sink
.notify(Params::Array(vec![Value::Number(button_code.into())]))
.wait()
{
Ok(_) => info!("Publishing button code to subscriber over ws."),
Err(_) => {
warn!("Failed to publish button code.");
break;
}
}
}
});
},
),
("remove_buttons", |_id: SubscriptionId, _| {
// unsubscribe
futures::future::ok(Value::Bool(true))
}),
);
let ws_server =
env::var("PEACH_BUTTONS_SERVER").unwrap_or_else(|_| "127.0.0.1:5111".to_string());
info!("Starting JSON-RPC server on {}.", ws_server);
let server = ServerBuilder::with_meta_extractor(io, |context: &RequestContext| {
Arc::new(Session::new(context.sender()))
})
.start(
&ws_server
.parse()
.expect("Invalid WS address and port combination"),
)
.expect("Unable to start RPC server");
info!("Listening for requests.");
server.wait().unwrap();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rpc_success() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_success_response", |_| {
Ok(Value::String("success".into()))
});
test::Rpc::from(io)
};
assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#);
}
}

12
peach-buttons/src/main.rs Normal file
View File

@ -0,0 +1,12 @@
use std::process;
use log::error;
fn main() {
env_logger::init();
if let Err(e) = peach_buttons::run() {
error!("Application error: {}", e);
process::exit(1);
}
}

4
peach-oled/.cargo/config Normal file
View File

@ -0,0 +1,4 @@
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
objcopy = { path ="aarch64-linux-gnu-objcopy" }
strip = { path ="aarch64-linux-gnu-strip" }

2
peach-oled/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
**/*.rs.bk

7
peach-oled/.travis.yml Normal file
View File

@ -0,0 +1,7 @@
language: rust
rust:
- nightly
before_script:
- rustup component add clippy
script:
- cargo clippy -- -D warnings

1603
peach-oled/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

42
peach-oled/Cargo.toml Normal file
View File

@ -0,0 +1,42 @@
[package]
name = "peach-oled"
version = "0.1.3"
authors = ["Andrew Reid <gnomad@cryptolab.net>"]
edition = "2018"
description = "Write and draw to OLED display using JSON-RPC over HTTP."
homepage = "https://opencollective.com/peachcloud"
repository = "https://github.com/peachcloud/peach-oled"
readme = "README.md"
license = "AGPL-3.0-only"
publish = false
[package.metadata.deb]
depends = "$auto"
extended-description = """\
peach-oled allows writing and drawing to a 128x64 pixel OLED display \
with SDD1306 driver (I2C) using JSON-RPC over HTTP."""
maintainer-scripts="debian"
systemd-units = { unit-name = "peach-oled" }
assets = [
["target/release/peach-oled", "usr/bin/", "755"],
["README.md", "usr/share/doc/peach-oled/README", "644"],
]
[badges]
travis-ci = { repository = "peachcloud/peach-oled", branch = "master" }
maintenance = { status = "actively-developed" }
[dependencies]
jsonrpc-core = "11.0.0"
jsonrpc-http-server = "11.0.0"
jsonrpc-test = "11.0.0"
linux-embedded-hal = "0.2.2"
embedded-graphics = "0.4.7"
tinybmp = "0.1.0"
ssd1306 = "0.2.6"
serde = { version = "1.0.87", features = ["derive"] }
serde_json = "1.0.39"
log = "0.4.0"
env_logger = "0.6.1"
snafu = "0.4.1"
nix="0.11"

165
peach-oled/README.md Normal file
View File

@ -0,0 +1,165 @@
# peach-oled
[![Build Status](https://travis-ci.com/peachcloud/peach-oled.svg?branch=master)](https://travis-ci.com/peachcloud/peach-oled) ![Generic badge](https://img.shields.io/badge/version-0.1.3-<COLOR>.svg)
OLED microservice module for PeachCloud. Write to a 128x64 OLED display with SDD1306 driver (I2C) using [JSON-RPC](https://www.jsonrpc.org/specification) over http.
![Close-up, black-and-white photo of an Adafruit 128x64 1.3" OLED Bonnet. The circuit board features a 5-way joystick on the left side, two push-buttons on the right side (labelled #5 and #6), and a central OLED display. The display shows text reading: "PeachCloud" on the first line and "IP: 192.168.0.8" on the third line. A circle is displayed beneath the two lines of text and is horizontally-centered".](docs/images/peachcloud_oled.jpg)
### JSON-RPC API
| Method | Parameters | Description |
| --- | --- | --- |
| `clear` | | Clear the display buffer |
| `draw` | `bytes`, `width`, `height`, `x_coord`, `y_coord` | Draw graphic to display buffer for given byte array, dimensions and co-ordinates |
| `flush` | | Flush the display |
| `ping` | | Respond with `success` if microservice is running |
| `power` | `on` | Toggle the display (memory is retained while off) |
| `write` | `x_coord`, `y_coord`, `string`, `font_size` | Write message to display buffer for given co-ordinates using given font size |
| Font Sizes |
| --- |
| `6x8` |
| `6x12` |
| `8x16` |
| `12x16` |
### Environment
The JSON-RPC HTTP server address and port can be configured with the `PEACH_OLED_SERVER` environment variable:
`export PEACH_OLED_SERVER=127.0.0.1:5000`
When not set, the value defaults to `127.0.0.1:5112`.
Logging is made available with `env_logger`:
`export RUST_LOG=info`
Other logging levels include `debug`, `warn` and `error`.
### Setup
Clone this repo:
`git clone https://github.com/peachcloud/peach-oled.git`
Move into the repo and compile:
`cd peach-oled`
`cargo build --release`
Run the binary:
`./target/release/peach-oled`
### Debian Packaging
A `systemd` service file and Debian maintainer scripts are included in the `debian` directory, allowing `peach-oled` to be easily bundled as a Debian package (`.deb`). The `cargo-deb` [crate](https://crates.io/crates/cargo-deb) can be used to achieve this.
Install `cargo-deb`:
`cargo install cargo-deb`
Move into the repo:
`cd peach-oled`
Build the package:
`cargo deb`
The output will be written to `target/debian/peach-oled_0.1.0_arm64.deb` (or similar).
Build the package (aarch64):
`cargo deb --target aarch64-unknown-linux-gnu`
Install the package as follows:
`sudo dpkg -i target/debian/peach-oled_0.1.0_arm64.deb`
The service will be automatically enabled and started.
Uninstall the service:
`sudo apt-get remove peach-oled`
Remove configuration files (not removed with `apt-get remove`):
`sudo apt-get purge peach-oled`
### Example Usage
**Write Text to the OLED Display**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "write", "params" : {"x_coord": 0, "y_coord": 0, "string": "Welcome to PeachCloud", "font_size": "6x8" }, "id":1 }' 127.0.0.1:5112`
Server responds with:
`{"jsonrpc":"2.0","result":success","id":1}`
OLED will remain blank because no flush command has been issued.
Write to the second line of the display:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "write", "params" : {"x_coord": 0, "y_coord": 8, "string": "Born in cypherspace", "font_size": "6x12" }, "id":1 }' 127.0.0.1:5112`
Flush the display:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "flush", "id":1 }' 127.0.0.1:5112`
OLED display shows:
`Welcome to PeachCloud!`
`Born in cypherspace`
Validation checks are performed for all three parameters: `x_coord`, `y_coord` and `string`. An appropriate error is returned if the validation checks are not satisfied:
`{"jsonrpc":"2.0","error":{"code":1,"message":"Validation error: coordinate x out of range 0-128: 129."},"id":1}`
`{"jsonrpc":"2.0","error":{"code":1,"message":"validation error","data":"y_coord not in range 0-57"},"id":1}`
`{"jsonrpc":"2.0","error":{"code":1,"message":"Validation error: string length 47 out of range 0-21."},"id":1}`
An error is returned if one or all of the expected parameters are not supplied:
`{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params: missing field `font_size`."},"id":1}`
-----
**Draw Graphic to the OLED Display**
With microservice running, open a second terminal window and use `curl` to call server methods:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "draw", "params" : {"bytes": [30, 0, 33, 0, 64, 128, 128, 64, 140, 64, 140, 64, 128, 64, 64, 128, 33, 0, 30, 0], "width": 10, "height": 10, "x_coord": 32, "y_coord": 32}, "id":1 }' 127.0.0.1:5112`
Server responds with:
`{"jsonrpc":"2.0","result":success","id":1}`
OLED will remain blank because no flush command has been issued.
Flush the display:
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "flush", "id":1 }' 127.0.0.1:5112`
OLED display shows a 10x10 graphic of a dot inside a circle.
No validation checks are currently performed on the parameters of the `draw` RPC, aside from type-checks when the parameters are parsed.
-----
**Clear the Display**
`curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "method": "clear", "id":1 }' 127.0.0.1:5112`
Server responds with:
`{"jsonrpc":"2,0","result":"success","id":1}`
### Licensing
AGPL-3.0

View File

@ -0,0 +1,27 @@
[Unit]
Description=JSON-RPC microservice for writing and drawing to an OLED display over HTTP.
[Service]
Type=simple
User=peach-oled
Group=i2c
Environment="RUST_LOG=error"
ExecStart=/usr/bin/peach-oled
Restart=always
CapabilityBoundingSet=~CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYS_BOOT CAP_SYS_TIME CAP_KILL CAP_WAKE_ALARM CAP_LINUX_IMMUTABLE CAP_BLOCK_SUSPEND CAP_LEASE CAP_SYS_NICE CAP_SYS_RESOURCE CAP_RAWIO CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_* CAP_FOWNER CAP_IPC_OWNER CAP_SETUID CAP_SETGID CAP_SETPCAP CAP_AUDIT_*
InaccessibleDirectories=/home
LockPersonality=yes
NoNewPrivileges=yes
PrivateTmp=yes
PrivateUsers=yes
ProtectControlGroups=yes
ProtectHome=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
ProtectSystem=yes
ReadOnlyDirectories=/var
RestrictAddressFamilies=~AF_INET6 AF_UNIX
SystemCallFilter=~@reboot @clock @debug @module @mount @swap @resources @privileged
[Install]
WantedBy=multi-user.target

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

72
peach-oled/src/error.rs Normal file
View File

@ -0,0 +1,72 @@
use std::error;
use jsonrpc_core::{types::error::Error, ErrorCode};
use linux_embedded_hal as hal;
use snafu::Snafu;
pub type BoxError = Box<dyn error::Error>;
#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum OledError {
#[snafu(display("Failed to create interface for I2C device: {}", source))]
I2CError {
source: hal::i2cdev::linux::LinuxI2CError,
},
#[snafu(display("Coordinate {} out of range {}: {}", coord, range, value))]
InvalidCoordinate {
coord: String,
range: String,
value: i32,
},
// TODO: implement for validate() in src/lib.rs
#[snafu(display("Font size invalid: {}", font))]
InvalidFontSize { font: String },
#[snafu(display("String length out of range 0-21: {}", len))]
InvalidString { len: usize },
#[snafu(display("Missing expected parameter: {}", e))]
MissingParameter { e: Error },
#[snafu(display("Failed to parse parameter: {}", e))]
ParseError { e: Error },
}
impl From<OledError> for Error {
fn from(err: OledError) -> Self {
match &err {
OledError::I2CError { source } => Error {
code: ErrorCode::ServerError(-32000),
message: format!("Failed to create interface for I2C device: {}", source),
data: None,
},
OledError::InvalidCoordinate {
coord,
value,
range,
} => Error {
code: ErrorCode::ServerError(-32001),
message: format!(
"Validation error: coordinate {} out of range {}: {}",
coord, range, value
),
data: None,
},
OledError::InvalidFontSize { font } => Error {
code: ErrorCode::ServerError(-32002),
message: format!("Validation error: {} is not an accepted font size. Use 6x8, 6x12, 8x16 or 12x16 instead", font),
data: None,
},
OledError::InvalidString { len } => Error {
code: ErrorCode::ServerError(-32003),
message: format!("Validation error: string length {} out of range 0-21", len),
data: None,
},
OledError::MissingParameter { e } => e.clone(),
OledError::ParseError { e } => e.clone(),
}
}
}

448
peach-oled/src/lib.rs Normal file
View File

@ -0,0 +1,448 @@
mod error;
use std::{
env, process,
result::Result,
sync::{Arc, Mutex},
};
use embedded_graphics::coord::Coord;
use embedded_graphics::fonts::{Font12x16, Font6x12, Font6x8, Font8x16};
use embedded_graphics::image::Image1BPP;
use embedded_graphics::prelude::*;
use hal::I2cdev;
use jsonrpc_core::{types::error::Error, IoHandler, Params, Value};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
#[allow(unused_imports)]
use jsonrpc_test as test;
use linux_embedded_hal as hal;
use log::{debug, error, info};
use serde::Deserialize;
use snafu::{ensure, ResultExt};
use ssd1306::prelude::*;
use ssd1306::Builder;
use crate::error::{BoxError, I2CError, InvalidCoordinate, InvalidString, OledError};
//define the Graphic struct for receiving draw commands
#[derive(Debug, Deserialize)]
pub struct Graphic {
bytes: Vec<u8>,
width: u32,
height: u32,
x_coord: i32,
y_coord: i32,
}
//define the Msg struct for receiving write commands
#[derive(Debug, Deserialize)]
pub struct Msg {
x_coord: i32,
y_coord: i32,
string: String,
font_size: String,
}
//definte the On struct for receiving power on/off commands
#[derive(Debug, Deserialize)]
pub struct On {
on: bool,
}
fn validate(m: &Msg) -> Result<(), OledError> {
ensure!(
m.string.len() <= 21,
InvalidString {
len: m.string.len()
}
);
ensure!(
m.x_coord >= 0,
InvalidCoordinate {
coord: "x".to_string(),
range: "0-128".to_string(),
value: m.x_coord,
}
);
ensure!(
m.x_coord < 129,
InvalidCoordinate {
coord: "x".to_string(),
range: "0-128".to_string(),
value: m.x_coord,
}
);
ensure!(
m.y_coord >= 0,
InvalidCoordinate {
coord: "y".to_string(),
range: "0-47".to_string(),
value: m.y_coord,
}
);
ensure!(
m.y_coord < 148,
InvalidCoordinate {
coord: "y".to_string(),
range: "0-47".to_string(),
value: m.y_coord,
}
);
Ok(())
}
pub fn run() -> Result<(), BoxError> {
info!("Starting up.");
debug!("Creating interface for I2C device.");
let i2c = I2cdev::new("/dev/i2c-1").context(I2CError)?;
let mut disp: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into();
info!("Initializing the display.");
disp.init().unwrap_or_else(|_| {
error!("Problem initializing the OLED display.");
process::exit(1);
});
debug!("Flushing the display.");
disp.flush().unwrap_or_else(|_| {
error!("Problem flushing the OLED display.");
process::exit(1);
});
let oled = Arc::new(Mutex::new(disp));
let oled_clone = Arc::clone(&oled);
info!("Creating JSON-RPC I/O handler.");
let mut io = IoHandler::default();
io.add_method("clear", move |_| {
let mut oled = oled_clone.lock().unwrap();
info!("Clearing the display.");
oled.clear();
info!("Flushing the display.");
oled.flush().unwrap_or_else(|_| {
error!("Problem flushing the OLED display.");
process::exit(1);
});
Ok(Value::String("success".into()))
});
let oled_clone = Arc::clone(&oled);
io.add_method("draw", move |params: Params| {
let g: Result<Graphic, Error> = params.parse();
let g: Graphic = g?;
// TODO: add simple byte validation function
let mut oled = oled_clone.lock().unwrap();
info!("Drawing image to the display.");
let im =
Image1BPP::new(&g.bytes, g.width, g.height).translate(Coord::new(g.x_coord, g.y_coord));
oled.draw(im.into_iter());
Ok(Value::String("success".into()))
});
let oled_clone = Arc::clone(&oled);
io.add_method("flush", move |_| {
let mut oled = oled_clone.lock().unwrap();
info!("Flushing the display.");
oled.flush().unwrap_or_else(|_| {
error!("Problem flushing the OLED display.");
process::exit(1);
});
Ok(Value::String("success".into()))
});
let oled_clone = Arc::clone(&oled);
io.add_method("ping", |_| Ok(Value::String("success".to_string())));
io.add_method("power", move |params: Params| {
let o: Result<On, Error> = params.parse();
let o: On = o?;
let mut oled = oled_clone.lock().unwrap();
if o.on {
info!("Turning the display on.");
} else {
info!("Turnin the display off.");
}
oled.display_on(o.on).unwrap_or_else(|_| {
error!("Problem turning the display on.");
process::exit(1);
});
Ok(Value::String("success".into()))
});
let oled_clone = Arc::clone(&oled);
io.add_method("write", move |params: Params| {
info!("Received a 'write' request.");
let m: Result<Msg, Error> = params.parse();
let m: Msg = m?;
validate(&m)?;
let mut oled = oled_clone.lock().unwrap();
info!("Writing to the display.");
if m.font_size == "6x8" {
oled.draw(
Font6x8::render_str(&m.string)
.translate(Coord::new(m.x_coord, m.y_coord))
.into_iter(),
);
} else if m.font_size == "6x12" {
oled.draw(
Font6x12::render_str(&m.string)
.translate(Coord::new(m.x_coord, m.y_coord))
.into_iter(),
);
} else if m.font_size == "8x16" {
oled.draw(
Font8x16::render_str(&m.string)
.translate(Coord::new(m.x_coord, m.y_coord))
.into_iter(),
);
} else if m.font_size == "12x16" {
oled.draw(
Font12x16::render_str(&m.string)
.translate(Coord::new(m.x_coord, m.y_coord))
.into_iter(),
);
}
Ok(Value::String("success".into()))
});
let http_server =
env::var("PEACH_OLED_SERVER").unwrap_or_else(|_| "127.0.0.1:5112".to_string());
info!("Starting JSON-RPC server on {}.", http_server);
let server = ServerBuilder::new(io)
.cors(DomainsValidation::AllowOnly(vec![
AccessControlAllowOrigin::Null,
]))
.start_http(
&http_server
.parse()
.expect("Invalid HTTP address and port combination"),
)
.expect("Unable to start RPC server");
info!("Listening for requests.");
server.wait();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use hal::i2cdev::linux::LinuxI2CError;
use jsonrpc_core::ErrorCode;
use nix::Error as NixError;
use std::io::Error as IoError;
use std::io::ErrorKind;
// test to ensure correct success response
#[test]
fn rpc_success() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_success_response", |_| {
Ok(Value::String("success".into()))
});
test::Rpc::from(io)
};
assert_eq!(rpc.request("rpc_success_response", &()), r#""success""#);
}
// test to ensure correct internal error response
#[test]
fn rpc_internal_error() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_internal_error", |_| Err(Error::internal_error()));
test::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_internal_error", &()),
r#"{
"code": -32603,
"message": "Internal error"
}"#
);
}
// test to ensure correct I2CError error response (io::Error variant)
#[test]
fn rpc_i2c_io_error() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_i2c_io_error", |_| {
let io_err = IoError::new(ErrorKind::PermissionDenied, "oh no!");
let source = LinuxI2CError::Io(io_err);
Err(Error::from(OledError::I2CError { source }))
});
test::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_i2c_io_error", &()),
r#"{
"code": -32000,
"message": "I2C device error: oh no!"
}"#
);
}
// test to ensure correct I2CError error response (nix::Error variant)
#[test]
fn rpc_i2c_nix_error() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_i2c_nix_error", |_| {
let nix_err = NixError::InvalidPath;
let source = LinuxI2CError::Nix(nix_err);
Err(Error::from(OledError::I2CError { source }))
});
test::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_i2c_nix_error", &()),
r#"{
"code": -32000,
"message": "I2C device error: Invalid path"
}"#
);
}
// test to ensure correct InvalidCoordinate error response
#[test]
fn rpc_invalid_coord() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_invalid_coord", |_| {
Err(Error::from(OledError::InvalidCoordinate {
coord: "x".to_string(),
range: "0-128".to_string(),
value: 321,
}))
});
test::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_invalid_coord", &()),
r#"{
"code": -32001,
"message": "Validation error: coordinate x out of range 0-128: 321"
}"#
);
}
// test to ensure correct InvalidFontSize error response
#[test]
fn rpc_invalid_fontsize() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_invalid_fontsize", |_| {
Err(Error::from(OledError::InvalidFontSize {
font: "24x32".to_string(),
}))
});
test::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_invalid_fontsize", &()),
r#"{
"code": -32002,
"message": "Validation error: 24x32 is not an accepted font size. Use 6x8, 6x12, 8x16 or 12x16 instead"
}"#
);
}
// test to ensure correct InvalidString error response
#[test]
fn rpc_invalid_string() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_invalid_string", |_| {
Err(Error::from(OledError::InvalidString { len: 22 }))
});
test::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_invalid_string", &()),
r#"{
"code": -32003,
"message": "Validation error: string length 22 out of range 0-21"
}"#
);
}
// test to ensure correct invalid parameters error response
#[test]
fn rpc_invalid_params() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_invalid_params", |_| {
let e = Error {
code: ErrorCode::InvalidParams,
message: String::from("invalid params"),
data: Some(Value::String(
"Invalid params: invalid type: null, expected struct Msg.".into(),
)),
};
Err(Error::from(OledError::MissingParameter { e }))
});
test::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_invalid_params", &()),
r#"{
"code": -32602,
"message": "invalid params",
"data": "Invalid params: invalid type: null, expected struct Msg."
}"#
);
}
// test to ensure correct parse error response
#[test]
fn rpc_parse_error() {
let rpc = {
let mut io = IoHandler::new();
io.add_method("rpc_parse_error", |_| {
let e = Error {
code: ErrorCode::ParseError,
message: String::from("Parse error"),
data: None,
};
Err(Error::from(OledError::ParseError { e }))
});
test::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_parse_error", &()),
r#"{
"code": -32700,
"message": "Parse error"
}"#
);
}
}

14
peach-oled/src/main.rs Normal file
View File

@ -0,0 +1,14 @@
use std::process;
use log::error;
fn main() {
// initialize the logger
env_logger::init();
// handle errors returned from `run`
if let Err(e) = peach_oled::run() {
error!("Application error: {}", e);
process::exit(1);
}
}