peach-workspace/peach-oled/src/lib.rs

423 lines
12 KiB
Rust

mod error;
use std::{
env, process,
result::Result,
sync::{Arc, Mutex},
};
use embedded_graphics::{
coord::Coord,
fonts::{Font12x16, Font6x12, Font6x8, Font8x16},
image::Image1BPP,
prelude::*,
};
use hal::I2cdev;
use jsonrpc_core::{types::error::Error, IoHandler, Params, Value};
use jsonrpc_http_server::{AccessControlAllowOrigin, DomainsValidation, ServerBuilder};
use linux_embedded_hal as hal;
use log::{debug, error, info};
use serde::Deserialize;
use ssd1306::{prelude::*, Builder};
use crate::error::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(msg: &Msg) -> Result<(), OledError> {
if msg.string.len() > 21 {
Err(OledError::InvalidString {
len: msg.string.len(),
})
} else if msg.x_coord < 0 || msg.x_coord > 128 {
Err(OledError::InvalidCoordinate {
coord: "x".to_string(),
range: "0-128".to_string(),
value: msg.x_coord,
})
} else if msg.y_coord < 0 || msg.y_coord > 147 {
Err(OledError::InvalidCoordinate {
coord: "y".to_string(),
range: "0-47".to_string(),
value: msg.y_coord,
})
} else {
Ok(())
}
}
pub fn run() -> Result<(), OledError> {
info!("Starting up.");
debug!("Creating interface for I2C device.");
let i2c = I2cdev::new("/dev/i2c-1").map_err(|source| OledError::I2CError { source })?;
let mut display: GraphicsMode<_> = Builder::new().connect_i2c(i2c).into();
info!("Initializing the display.");
display.init().unwrap_or_else(|_| {
error!("Problem initializing the OLED display.");
process::exit(1);
});
debug!("Flushing the display.");
display.flush().unwrap_or_else(|_| {
error!("Problem flushing the OLED display.");
process::exit(1);
});
let oled = Arc::new(Mutex::new(display));
let oled_clone = Arc::clone(&oled);
info!("Creating JSON-RPC I/O handler.");
let mut io = IoHandler::default();
io.add_sync_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_sync_method("draw", move |params: Params| {
let graphic: Graphic = params.parse()?;
// TODO: add simple byte validation function
let mut oled = oled_clone.lock().unwrap();
info!("Drawing image to the display.");
let image = Image1BPP::new(&graphic.bytes, graphic.width, graphic.height)
.translate(Coord::new(graphic.x_coord, graphic.y_coord));
oled.draw(image.into_iter());
Ok(Value::String("success".into()))
});
let oled_clone = Arc::clone(&oled);
io.add_sync_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_sync_method("ping", |_| Ok(Value::String("success".to_string())));
io.add_sync_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_sync_method("write", move |params: Params| {
info!("Received a 'write' request.");
let msg = params.parse()?;
validate(&msg)?;
let mut oled = oled_clone.lock().unwrap();
info!("Writing to the display.");
if msg.font_size == "6x8" {
oled.draw(
Font6x8::render_str(&msg.string)
.translate(Coord::new(msg.x_coord, msg.y_coord))
.into_iter(),
);
} else if msg.font_size == "6x12" {
oled.draw(
Font6x12::render_str(&msg.string)
.translate(Coord::new(msg.x_coord, msg.y_coord))
.into_iter(),
);
} else if msg.font_size == "8x16" {
oled.draw(
Font8x16::render_str(&msg.string)
.translate(Coord::new(msg.x_coord, msg.y_coord))
.into_iter(),
);
} else if msg.font_size == "12x16" {
oled.draw(
Font12x16::render_str(&msg.string)
.translate(Coord::new(msg.x_coord, msg.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 jsonrpc_test as test_rpc;
//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_sync_method("rpc_success_response", |_| {
Ok(Value::String("success".into()))
});
test_rpc::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_sync_method("rpc_internal_error", |_| Err(Error::internal_error()));
test_rpc::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_sync_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::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_i2c_io_error", &()),
r#"{
"code": -32000,
"message": "Failed to create interface for I2C device: oh no!"
}"#
);
}
/* TODO: fix this test (update nix deps)
// 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_sync_method("rpc_i2c_nix_error", |_| {
let nix_err = NixError::InvalidPath;
let source = LinuxI2CError::Nix(nix_err);
Err(Error::from(OledError::I2CError { source }))
});
test_rpc::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_sync_method("rpc_invalid_coord", |_| {
Err(Error::from(OledError::InvalidCoordinate {
coord: "x".to_string(),
range: "0-128".to_string(),
value: 321,
}))
});
test_rpc::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_sync_method("rpc_invalid_fontsize", |_| {
Err(Error::from(OledError::InvalidFontSize {
font: "24x32".to_string(),
}))
});
test_rpc::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_sync_method("rpc_invalid_string", |_| {
Err(Error::from(OledError::InvalidString { len: 22 }))
});
test_rpc::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_sync_method("rpc_invalid_params", |_| {
let source = 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 { source }))
});
test_rpc::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_sync_method("rpc_parse_error", |_| {
let source = Error {
code: ErrorCode::ParseError,
message: String::from("Parse error"),
data: None,
};
Err(Error::from(OledError::ParseError { source }))
});
test_rpc::Rpc::from(io)
};
assert_eq!(
rpc.request("rpc_parse_error", &()),
r#"{
"code": -32700,
"message": "Parse error"
}"#
);
}
}