Initial Commit
This commit is contained in:
commit
40a109637f
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/target
|
||||
|
||||
|
||||
# Added by cargo
|
||||
#
|
||||
# already existing elements were commented out
|
||||
|
||||
#/target
|
1724
Cargo.lock
generated
Normal file
1724
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "neolights"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
smart-leds = "0.3.0"
|
||||
ws281x-rpi = "0.0.1"
|
||||
rocket = { version="0.5.0-rc.2", features = ["json"] }
|
||||
# rusqlite = { version = "0.28.0", features = ["bundled"] }
|
||||
r2d2 = "0.8.10"
|
||||
r2d2_sqlite = { version = "0.21.0", features = ["bundled"] }
|
||||
tokio = { version = "1.21.1", features = ["full"] }
|
||||
serde = "1.0.145"
|
||||
|
393
src/animations.rs
Normal file
393
src/animations.rs
Normal file
@ -0,0 +1,393 @@
|
||||
use std::{u8, error::Error};
|
||||
|
||||
use rocket::{serde::{json::{Json, serde_json::json}, Deserialize, Serialize}, State};
|
||||
|
||||
use crate::{utils::ApiResponse, DBConn, ASender, Action};
|
||||
use crate::DBPool;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LigthSetting {
|
||||
start: Option<u32>,
|
||||
end: Option<u32>,
|
||||
tags: Option<String>,
|
||||
r: u8,
|
||||
b: u8,
|
||||
g: u8,
|
||||
}
|
||||
|
||||
impl LigthSetting {
|
||||
pub fn get_start(&self) -> Option<u32>{
|
||||
self.start
|
||||
}
|
||||
pub fn get_end(&self) -> Option<u32>{
|
||||
self.end
|
||||
}
|
||||
pub fn get_tags(&self) -> Option<String>{
|
||||
if self.tags.as_ref().is_some() {
|
||||
return Some(self.tags.as_ref().unwrap().to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn get_r(&self) -> u8 {
|
||||
self.r
|
||||
}
|
||||
pub fn get_b(&self) -> u8 {
|
||||
self.b
|
||||
}
|
||||
pub fn get_g(&self) -> u8 {
|
||||
self.g
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct KeyFrame {
|
||||
duration: u32,
|
||||
settings: Vec<LigthSetting>
|
||||
}
|
||||
|
||||
impl KeyFrame {
|
||||
pub fn get_settings(&self) -> Vec<LigthSetting> {
|
||||
self.settings.clone()
|
||||
}
|
||||
|
||||
pub fn get_duration(&self) -> u32 {
|
||||
self.duration
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Animation {
|
||||
key_frames: Vec<KeyFrame>,
|
||||
priority: Option<u32>,
|
||||
name: String,
|
||||
repeat: bool,
|
||||
}
|
||||
|
||||
impl Animation {
|
||||
|
||||
pub fn get_frames(&self) -> Vec<KeyFrame> {
|
||||
self.key_frames.clone()
|
||||
}
|
||||
|
||||
pub fn get_name(&self) -> String {
|
||||
self.name.to_string()
|
||||
}
|
||||
|
||||
pub fn get_priority(&self) -> u32 {
|
||||
if self.priority.is_none() { 0 } else {self.priority.unwrap()}
|
||||
}
|
||||
|
||||
pub fn get_repeat(&self) -> bool {
|
||||
self.repeat
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/animation", data= "<data>")]
|
||||
pub async fn animation(data: Json<Animation>, db: &State<DBPool>) -> String {
|
||||
|
||||
if data.key_frames.is_empty() {
|
||||
return json!(ApiResponse {code: 400, message: "KeyFrame are nessesary".to_string()}).to_string();
|
||||
}
|
||||
if data.name.is_empty() {
|
||||
return json!(ApiResponse {code: 400, message: "Name must not be empty".to_string()}).to_string();
|
||||
}
|
||||
|
||||
for key_frame in data.key_frames.iter() {
|
||||
if key_frame.settings.is_empty() {
|
||||
return json!(ApiResponse {code: 400, message: "keyFrames.settings can not be empty".to_string()}).to_string();
|
||||
}
|
||||
|
||||
if key_frame.duration == 0 {
|
||||
return json!(ApiResponse {code: 400, message: "keyFrames.duration can not be 0".to_string()}).to_string();
|
||||
}
|
||||
|
||||
for setting in key_frame.settings.iter() {
|
||||
|
||||
if setting.tags.is_none() && (setting.start.is_none() || setting.end.is_none()) {
|
||||
return json!(ApiResponse {code: 400, message: "Either tags or start end pair is needed".to_string()}).to_string();
|
||||
}
|
||||
|
||||
if !(setting.start.is_none() && setting.end.is_none()) && (setting.start.is_none() || setting.end.is_none()) {
|
||||
return json!(ApiResponse {code: 400, message: "Both start and end need to be filled".to_string()}).to_string();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let db_status = add_animation_to_db(data, db);
|
||||
|
||||
if db_status.is_err() || db_status.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: format!("Error setting the animation. Err: {:?}", db_status.err()).to_string()}).to_string();
|
||||
}
|
||||
|
||||
let db_status = db_status.ok().unwrap();
|
||||
|
||||
if !db_status.is_empty() {
|
||||
return db_status;
|
||||
}
|
||||
|
||||
json!(ApiResponse {code: 200, message: "Configuration was successful".to_string()}).to_string()
|
||||
}
|
||||
|
||||
fn get_animation_from_name(conn: &DBConn, name: String) -> Result<Option<u32>, Box<dyn Error>> {
|
||||
let mut stmt = conn.prepare("SELECT id FROM animation WHERE name=?1")?;
|
||||
|
||||
let next = stmt.query_map([name], |row| {
|
||||
row.get(0)
|
||||
})?.next();
|
||||
|
||||
if next.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(next.unwrap()?)
|
||||
}
|
||||
|
||||
fn get_keyframe_from_index(conn: &DBConn, animation: u32, index: u32) -> Result<Option<u32>, Box<dyn Error>> {
|
||||
let mut stmt = conn.prepare("SELECT id FROM keyframe WHERE animation=?1 AND i=?2")?;
|
||||
|
||||
let next = stmt.query_map([animation, index], |row| {
|
||||
row.get(0)
|
||||
})?.next();
|
||||
|
||||
if next.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(next.unwrap()?)
|
||||
}
|
||||
|
||||
fn add_animation_to_db(data: Json<Animation>, pool: &State<DBPool>) -> Result<String, Box<dyn Error>>{
|
||||
let priority = if data.priority.is_some() { data.priority.unwrap() } else { 0 };
|
||||
|
||||
let conn = pool.get()?;
|
||||
|
||||
println!("g: {}", data.name);
|
||||
|
||||
let name = get_animation_from_name(&conn, data.name.to_string())?;
|
||||
|
||||
println!("t: {:?}", name);
|
||||
|
||||
if name.is_some() {
|
||||
return Ok(json!(ApiResponse {code: 500, message: "Animation with that name is already exists.".to_string()}).to_string());
|
||||
}
|
||||
|
||||
conn.execute("INSERT INTO animation (priority, name, repeat) VALUES (?1, ?2, ?3);", (priority, data.name.to_string(), data.repeat ))?;
|
||||
|
||||
let name = get_animation_from_name(&conn, data.name.to_string())?;
|
||||
if name.is_none() {
|
||||
return Ok(json!(ApiResponse {code: 500, message: "Failed to create animation.".to_string()}).to_string());
|
||||
}
|
||||
let animation_id = name.unwrap();
|
||||
|
||||
let mut i = 0;
|
||||
|
||||
for key_frame in data.key_frames.clone() {
|
||||
conn.execute("INSERT INTO keyframe (duration, animation, i) VALUES (?1, ?2, ?3);", (key_frame.duration, animation_id, i))?;
|
||||
|
||||
let keyframe = get_keyframe_from_index(&conn, animation_id, i)?;
|
||||
if keyframe.is_none() {
|
||||
return Ok(json!(ApiResponse {code: 500, message: "Failed to create animation.".to_string()}).to_string());
|
||||
}
|
||||
let keyframe = keyframe.unwrap();
|
||||
|
||||
for setting in key_frame.settings {
|
||||
conn.execute("INSERT INTO lightsetting (start, end, tags, r, b, g, keyframe) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);", (setting.start, setting.end, setting.tags, setting.r, setting.b, setting.g, keyframe))?;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Ok("".to_string())
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Stop animations
|
||||
*
|
||||
*/
|
||||
#[get("/stop/<name>")]
|
||||
pub async fn stop_animation(name: &str, stop_sender: &State<ASender<String>>) -> String {
|
||||
|
||||
let r = stop_sender.lock().unwrap().send(name.to_string());
|
||||
|
||||
if r.is_err() || r.ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: "Something went wrong".to_string()}).to_string()
|
||||
}
|
||||
|
||||
json!(ApiResponse {code: 200, message: "Configuration was successful".to_string()}).to_string()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Start animations
|
||||
*
|
||||
*/
|
||||
#[get("/start/<name>")]
|
||||
pub async fn start_animation(name: &str, start_sender: &State<ASender<Animation>>, db: &State<DBPool>) -> String {
|
||||
|
||||
let animation = get_animation(name, db);
|
||||
|
||||
if animation.is_err() || animation.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: format!("Probelms with the db: {:?}", animation.err()).to_string()}).to_string()
|
||||
}
|
||||
|
||||
let animation = animation.ok().unwrap();
|
||||
|
||||
if animation.is_none() {
|
||||
return json!(ApiResponse {code: 404, message: "Animation not found".to_string()}).to_string()
|
||||
}
|
||||
|
||||
let animation = animation.unwrap();
|
||||
|
||||
let r = start_sender.lock().unwrap().send(animation);
|
||||
if r.is_err() || r.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: "Probelms with the db".to_string()}).to_string()
|
||||
}
|
||||
|
||||
json!(ApiResponse {code: 200, message: "Started animation".to_string()}).to_string()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Clear animations
|
||||
*
|
||||
*/
|
||||
#[get("/clear/<name>")]
|
||||
pub async fn clear(name: &str, start_sender: &State<ASender<Animation>>, db: &State<DBPool>, action: &State<ASender<Action>>) -> String {
|
||||
|
||||
let r = action.lock().unwrap().send(Action::Clear);
|
||||
|
||||
if r.is_err() || r.ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: "Something went wrong".to_string()}).to_string()
|
||||
}
|
||||
|
||||
let animation = get_animation(name, db);
|
||||
|
||||
if animation.is_err() || animation.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: format!("Probelms with the db: {:?}", animation.err()).to_string()}).to_string()
|
||||
}
|
||||
|
||||
let animation = animation.ok().unwrap();
|
||||
|
||||
if animation.is_none() {
|
||||
return json!(ApiResponse {code: 404, message: "Animation not found".to_string()}).to_string()
|
||||
}
|
||||
|
||||
let animation = animation.unwrap();
|
||||
|
||||
let r = start_sender.lock().unwrap().send(animation);
|
||||
if r.is_err() || r.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: "Probelms with the db".to_string()}).to_string()
|
||||
}
|
||||
|
||||
json!(ApiResponse {code: 200, message: "Started animation".to_string()}).to_string()
|
||||
}
|
||||
|
||||
pub fn get_animation(name: &str, db: &State<DBPool>) -> Result<Option<Animation>, Box<dyn Error>> {
|
||||
|
||||
let conn = db.get()?;
|
||||
|
||||
let mut stmt = conn.prepare("SELECT id, priority, name, repeat FROM animation WHERE name=?1;")?;
|
||||
|
||||
struct AnimationId {
|
||||
animation: Animation,
|
||||
id: u8
|
||||
};
|
||||
|
||||
let next = stmt.query_map([name], |row| {
|
||||
Ok(AnimationId {
|
||||
animation: Animation {
|
||||
key_frames: Vec::new(),
|
||||
priority: Some(row.get(1)?),
|
||||
name: row.get(2)?,
|
||||
repeat: row.get(3)?
|
||||
},
|
||||
id: row.get(0)?
|
||||
})
|
||||
})?.next();
|
||||
|
||||
if next.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut animation = next.unwrap()?;
|
||||
|
||||
struct KeyFrameId {
|
||||
keyframe: KeyFrame,
|
||||
id: u8,
|
||||
}
|
||||
|
||||
let mut stmt = conn.prepare("SELECT id, duration from keyframe where animation=?1 order by i asc")?;
|
||||
|
||||
let mut map = stmt.query_map([animation.id], |row| {
|
||||
Ok(KeyFrameId {
|
||||
id: row.get(0)?,
|
||||
keyframe: KeyFrame {
|
||||
duration: row.get(1)?,
|
||||
settings: Vec::new()
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
while let Some(key_frame_id) = map.next() {
|
||||
let mut key_frame_id = key_frame_id?;
|
||||
|
||||
let mut stmt = conn.prepare("SELECT start, end, tags, r, b, g from lightsetting where keyframe=?1")?;
|
||||
|
||||
let mut light_setting_map = stmt.query_map([key_frame_id.id], |row| {
|
||||
Ok(LigthSetting {
|
||||
start: row.get(0)?,
|
||||
end: row.get(1)?,
|
||||
tags: row.get(2)?,
|
||||
r: row.get(3)?,
|
||||
b: row.get(4)?,
|
||||
g: row.get(5)?
|
||||
})
|
||||
})?;
|
||||
|
||||
while let Some(light_setting) = light_setting_map.next() {
|
||||
key_frame_id.keyframe.settings.push(light_setting?);
|
||||
}
|
||||
|
||||
animation.animation.key_frames.push(key_frame_id.keyframe);
|
||||
}
|
||||
|
||||
Ok(Some(animation.animation))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Delete animations
|
||||
*
|
||||
*/
|
||||
#[get("/delete/<name>")]
|
||||
pub async fn delete(name: &str, start_sender: &State<ASender<Animation>>, db: &State<DBPool>) -> String {
|
||||
|
||||
let animation = remove_animation(name, db);
|
||||
|
||||
if animation.is_err() || animation.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: format!("Probelms with the db: {:?}", animation.err()).to_string()}).to_string()
|
||||
}
|
||||
|
||||
json!(ApiResponse {code: 200, message: "Deleted animation".to_string()}).to_string()
|
||||
}
|
||||
|
||||
pub fn remove_animation(name: &str, db: &State<DBPool>) -> Result<(), Box<dyn Error>> {
|
||||
|
||||
let conn = db.get()?;
|
||||
|
||||
conn.execute("delete from lightsetting where id in (select l.id from lightsetting as l left join keyframe as k on l.keyframe = k.id left join animation as a on k.animation = a.id where a.name=?1);", [name])?;
|
||||
conn.execute("delete from keyframe where id in (select k.id from keyframe as k left join animation as a on k.animation = a.id where a.name=?1);", [name])?;
|
||||
conn.execute("delete from animation where name=?1;", [name])?;
|
||||
|
||||
Ok(())
|
||||
}
|
102
src/configure.rs
Normal file
102
src/configure.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use rocket::{serde::{json::{Json, serde_json::json}, Deserialize, Serialize}, State};
|
||||
|
||||
use crate::{utils::ApiResponse, Action, ASender};
|
||||
use crate::DBPool;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LightConfiguration {
|
||||
number: u32,
|
||||
direction_x: u32,
|
||||
direction_y: u32,
|
||||
direction_z: u32,
|
||||
tags: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LightConfigurationRequest {
|
||||
lights: Vec<LightConfiguration>
|
||||
}
|
||||
|
||||
#[post("/configure", data= "<data>")]
|
||||
pub async fn configure(data: Json<LightConfigurationRequest>, db: &State<DBPool>, action: &State<ASender<Action>>) -> String {
|
||||
|
||||
let conn = db.get();
|
||||
|
||||
if conn.is_err() || conn.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: "Clould not configure. Err: 1".to_string()}).to_string();
|
||||
}
|
||||
|
||||
let conn = conn.ok().unwrap();
|
||||
|
||||
let stmt = conn.prepare("SELECT configured from meta");
|
||||
|
||||
if stmt.is_err() || stmt.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: format!("Clould not configure. Err: 2, {:?}", stmt)}).to_string();
|
||||
}
|
||||
|
||||
let mut stmt = stmt.ok().unwrap();
|
||||
|
||||
struct Data {
|
||||
configured: bool,
|
||||
}
|
||||
|
||||
let version_iter = stmt.query_map([], |row| {
|
||||
Ok(Data {
|
||||
configured: row.get(0)?
|
||||
})
|
||||
});
|
||||
|
||||
if version_iter.is_err() || version_iter.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: "Clould not configure. Err: 3".to_string()}).to_string();
|
||||
}
|
||||
|
||||
let version_iter = version_iter.ok().unwrap();
|
||||
|
||||
let mut recreate = false;
|
||||
|
||||
for data in version_iter {
|
||||
if data.unwrap().configured {
|
||||
println!("Already configured deleting current");
|
||||
recreate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
drop(stmt);
|
||||
|
||||
if recreate {
|
||||
let stmt = conn.execute("DELETE FROM configuration;", ());
|
||||
if stmt.is_err() || stmt.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: format!("Clould not configure. Err: 4, {:?}", stmt)}).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
for light in data.lights.iter() {
|
||||
|
||||
let mut tags: String = "".to_string();
|
||||
|
||||
if light.tags.is_some() {
|
||||
tags = light.tags.as_ref().unwrap().to_string()
|
||||
}
|
||||
|
||||
let stmt = conn.execute(
|
||||
"INSERT INTO configuration (ledcount, directionX, directionY, directionZ, tags) VALUES (?1, ?2, ?3, ?4, ?5);",
|
||||
(light.number, light.direction_x, light.direction_y, light.direction_z, tags));
|
||||
|
||||
if stmt.is_err() || stmt.as_ref().ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: format!("Clould not configure. Err: 5, {:?}", stmt)}).to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let send = action.lock().unwrap().send(Action::Reload);
|
||||
|
||||
if send.is_err() || send.ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: "Failed to reload".to_string()}).to_string()
|
||||
}
|
||||
|
||||
json!(ApiResponse {code: 200, message: "Configuration was successful".to_string()}).to_string()
|
||||
}
|
354
src/main.rs
Normal file
354
src/main.rs
Normal file
@ -0,0 +1,354 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
//use std::{thread, time::Duration, sync::Arc, io};
|
||||
use ::std::sync::Arc;
|
||||
|
||||
mod animations;
|
||||
mod configure;
|
||||
mod render;
|
||||
mod utils;
|
||||
|
||||
use animations::Animation;
|
||||
use r2d2::PooledConnection;
|
||||
use utils::ApiResponse;
|
||||
|
||||
use rocket::{State, serde::json::serde_json::json};
|
||||
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
|
||||
//use std::sync::atomic::AtomicBool;
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
sync::{
|
||||
mpsc::{self, Receiver, Sender},
|
||||
Mutex,
|
||||
}
|
||||
};
|
||||
|
||||
pub type DBPool = Arc<r2d2::Pool<r2d2_sqlite::SqliteConnectionManager>>;
|
||||
pub type DBConn = PooledConnection<r2d2_sqlite::SqliteConnectionManager>;
|
||||
pub type GResult = Result<(), Box<dyn Error>>;
|
||||
pub type ASender<T> = Arc<Mutex<Sender<T>>>;
|
||||
|
||||
#[get("/")]
|
||||
fn index() -> &'static str {
|
||||
"Hello World"
|
||||
}
|
||||
|
||||
pub enum Action {
|
||||
Reload,
|
||||
Stop,
|
||||
Clear
|
||||
}
|
||||
|
||||
#[get("/quit")]
|
||||
async fn quit(shutdown: rocket::Shutdown, action: &State<ASender<Action>>) -> String {
|
||||
println!("Got Shutdown");
|
||||
|
||||
let send = action.lock().unwrap().send(Action::Stop);
|
||||
|
||||
if send.is_err() || send.ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: "Failed to stop".to_string()}).to_string()
|
||||
}
|
||||
|
||||
shutdown.notify();
|
||||
|
||||
json!(ApiResponse {code: 200, message: "Stoped".to_string()}).to_string()
|
||||
}
|
||||
|
||||
#[get("/reload")]
|
||||
async fn reload(action: &State<ASender<Action>>) -> String {
|
||||
println!("Got Shutdown");
|
||||
|
||||
let send = action.lock().unwrap().send(Action::Reload);
|
||||
|
||||
if send.is_err() || send.ok().is_none() {
|
||||
return json!(ApiResponse {code: 500, message: "Failed to reload".to_string()}).to_string()
|
||||
}
|
||||
|
||||
json!(ApiResponse {code: 200, message: "Reloaded".to_string()}).to_string()
|
||||
}
|
||||
|
||||
fn ligth_controll(
|
||||
pool: DBPool,
|
||||
action: Receiver<Action>,
|
||||
start_ani: Receiver<Animation>,
|
||||
stop_ani: Receiver<String>,
|
||||
) {
|
||||
println!("Data loop started");
|
||||
|
||||
let render = render::Render::new(pool.clone());
|
||||
|
||||
if render.is_err() || render.as_ref().ok().is_none() {
|
||||
println!("something every wrong {:?}", render.err());
|
||||
return;
|
||||
}
|
||||
|
||||
let mut render = render.ok().unwrap();
|
||||
|
||||
'mainloop: loop {
|
||||
let action = action.try_recv();
|
||||
|
||||
if action.is_ok() {
|
||||
match action.ok().unwrap() {
|
||||
Action::Reload => {
|
||||
let result = render.load_config();
|
||||
if result.is_err() || result.as_ref().ok().is_none() {
|
||||
println!("Ehh failed to load config i guess: {:?}", result.err());
|
||||
}
|
||||
},
|
||||
Action::Clear => {
|
||||
render.blank();
|
||||
},
|
||||
Action::Stop => {
|
||||
render.blank();
|
||||
render.render();
|
||||
break 'mainloop;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let ani = start_ani.try_recv();
|
||||
|
||||
if ani.is_ok() {
|
||||
println!("Added animation");
|
||||
//TODO
|
||||
render.add_animation(ani.ok().unwrap());
|
||||
//ani.ok().unwrap().
|
||||
}
|
||||
|
||||
let ani = stop_ani.try_recv();
|
||||
|
||||
if ani.is_ok() {
|
||||
render.remove_animation(ani.ok().unwrap());
|
||||
}
|
||||
|
||||
render.render();
|
||||
|
||||
}
|
||||
|
||||
println!("stoped main loop");
|
||||
}
|
||||
|
||||
const DATAVERSION: &str = "0.0.10";
|
||||
|
||||
fn setup_database(pool: DBPool) -> GResult {
|
||||
let conn = pool.get()?;
|
||||
|
||||
println!("Trying to get meta data");
|
||||
|
||||
let stmt = conn.prepare("SELECT version from meta");
|
||||
|
||||
if stmt.is_err() || stmt.as_ref().ok().is_none() {
|
||||
return create_database(pool);
|
||||
}
|
||||
|
||||
let mut stmt = stmt?;
|
||||
|
||||
struct Data {
|
||||
version: String,
|
||||
}
|
||||
|
||||
let version_iter = stmt.query_map([], |row| {
|
||||
Ok(Data {
|
||||
version: row.get(0)?,
|
||||
})
|
||||
});
|
||||
|
||||
println!("Process meta");
|
||||
|
||||
if version_iter.is_err() || version_iter.as_ref().ok().is_none() {
|
||||
create_database(pool)?;
|
||||
} else {
|
||||
let version_iter = version_iter.ok().unwrap();
|
||||
|
||||
let mut recreate = true;
|
||||
|
||||
for data in version_iter {
|
||||
let version = data.unwrap().version;
|
||||
println!("Found version {}", version);
|
||||
|
||||
if !DATAVERSION.to_string().eq(&version) {
|
||||
println!("Mismatched versions recreating database");
|
||||
recreate = true;
|
||||
break;
|
||||
}
|
||||
|
||||
recreate = false;
|
||||
}
|
||||
|
||||
if recreate {
|
||||
create_database(pool)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_database(pool: DBPool) -> GResult {
|
||||
let conn = pool.get()?;
|
||||
|
||||
println!("Createing the database");
|
||||
|
||||
println!("Createing new meta table. With version {}", DATAVERSION);
|
||||
println!("Drop old meta table");
|
||||
conn.execute("DROP TABLE IF EXISTS meta;", ())?;
|
||||
// Create the database
|
||||
println!("create new meta table");
|
||||
conn.execute(
|
||||
"CREATE TABLE meta (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version TEXT,
|
||||
configured Boolean
|
||||
);",
|
||||
(),
|
||||
)?;
|
||||
println!("insert data");
|
||||
conn.execute(
|
||||
"INSERT INTO meta (version, configured) VALUES (?1, ?2);",
|
||||
(&DATAVERSION, false),
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"Createing new configuration table. With version {}",
|
||||
DATAVERSION
|
||||
);
|
||||
println!("Drop configuration table");
|
||||
conn.execute("DROP TABLE IF EXISTS configuration", ())?;
|
||||
println!("Create new configuration table");
|
||||
conn.execute(
|
||||
"CREATE TABLE configuration (
|
||||
id INTEGER PRIMARY KEY,
|
||||
ledcount INTEGER,
|
||||
directionX INTEGER,
|
||||
directionY INTEGER,
|
||||
directionZ INTEGER,
|
||||
tags VARCHAR(255)
|
||||
);",
|
||||
(),
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"Createing new Animation table. With version {}",
|
||||
DATAVERSION
|
||||
);
|
||||
println!("Drop Animation table and references");
|
||||
conn.execute("DROP TABLE IF EXISTS lightsetting", ())?;
|
||||
conn.execute("DROP TABLE IF EXISTS keyframe", ())?;
|
||||
conn.execute("DROP TABLE IF EXISTS animation", ())?;
|
||||
println!("Create new animation table");
|
||||
conn.execute(
|
||||
"CREATE TABLE animation (
|
||||
id INTEGER PRIMARY KEY,
|
||||
priority INTEGER,
|
||||
name VARCHAR(255),
|
||||
repeat BOOLEAN
|
||||
);",
|
||||
(),
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"Createing new key_frame table. With version {}",
|
||||
DATAVERSION
|
||||
);
|
||||
println!("Drop key_frame table");
|
||||
println!("Create new key_frame table");
|
||||
conn.execute(
|
||||
"CREATE TABLE keyframe (
|
||||
id INTEGER PRIMARY KEY,
|
||||
i INTEGER,
|
||||
duration INTEGER,
|
||||
animation INTEGER,
|
||||
FOREIGN KEY(animation) REFERENCES animation(id) ON DELETE CASCADE
|
||||
);",
|
||||
(),
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"Createing new LigthSetting table. With version {}",
|
||||
DATAVERSION
|
||||
);
|
||||
println!("Drop LigthSetting table");
|
||||
println!("Create new light_setting table");
|
||||
conn.execute(
|
||||
"CREATE TABLE lightsetting (
|
||||
id INTEGER PRIMARY KEY,
|
||||
start INTEGER,
|
||||
end INTEGER,
|
||||
tags VARCHAR(255),
|
||||
r INTEGER,
|
||||
b INTEGER,
|
||||
g INTEGER,
|
||||
keyframe INTEGER,
|
||||
FOREIGN KEY(keyframe) REFERENCES keyframe(id) ON DELETE CASCADE
|
||||
);",
|
||||
(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[rocket::main]
|
||||
async fn main() -> GResult {
|
||||
println!("Program start");
|
||||
|
||||
println!("Try to connect db");
|
||||
let sqlite_conn_manager = SqliteConnectionManager::file("ligths.db");
|
||||
let conn_pool = r2d2::Pool::new(sqlite_conn_manager).expect("Failed to create pool");
|
||||
let conn_pool_arc = Arc::new(conn_pool);
|
||||
|
||||
{
|
||||
let pool = conn_pool_arc.clone();
|
||||
let dd = setup_database(pool);
|
||||
println!("{:?}", dd);
|
||||
dd.ok().unwrap();
|
||||
}
|
||||
|
||||
let (action_sender, action_receiver): (Sender<Action>, Receiver<Action>) = mpsc::channel();
|
||||
let (start_animation_sender, start_animation_receiver): (
|
||||
Sender<Animation>,
|
||||
Receiver<Animation>,
|
||||
) = mpsc::channel();
|
||||
let (stop_animation_sender, stop_animation_receiver): (Sender<String>, Receiver<String>) =
|
||||
mpsc::channel();
|
||||
|
||||
let pool_clone = conn_pool_arc.clone();
|
||||
|
||||
let ligths_controll = std::thread::spawn(move || {
|
||||
ligth_controll(
|
||||
pool_clone,
|
||||
action_receiver,
|
||||
start_animation_receiver,
|
||||
stop_animation_receiver,
|
||||
);
|
||||
});
|
||||
|
||||
let cfg = rocket::Config::figment()
|
||||
.merge(("port", 3000))
|
||||
.merge(("address", "0.0.0.0"));
|
||||
|
||||
let _rocket = rocket::custom(cfg)
|
||||
.mount(
|
||||
"/",
|
||||
routes![
|
||||
index,
|
||||
quit,
|
||||
reload,
|
||||
configure::configure,
|
||||
animations::animation,
|
||||
animations::start_animation,
|
||||
animations::stop_animation,
|
||||
animations::clear,
|
||||
animations::delete
|
||||
],
|
||||
)
|
||||
.manage(conn_pool_arc.clone())
|
||||
.manage(Arc::new(Mutex::new(action_sender)))
|
||||
.manage(Arc::new(Mutex::new(start_animation_sender)))
|
||||
.manage(Arc::new(Mutex::new(stop_animation_sender)))
|
||||
.launch()
|
||||
.await?;
|
||||
ligths_controll.join().unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
290
src/render.rs
Normal file
290
src/render.rs
Normal file
@ -0,0 +1,290 @@
|
||||
use ws281x_rpi::Ws2812Rpi;
|
||||
|
||||
use crate::{DBPool, animations::Animation};
|
||||
|
||||
use std::error::Error;
|
||||
use smart_leds::{RGB8, SmartLedsWrite};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct LocalLedConfiguration {
|
||||
tag: Option<String>,
|
||||
start: usize,
|
||||
end: usize
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LigthSetting {
|
||||
start: Option<usize>,
|
||||
end: Option<usize>,
|
||||
tags: Option<String>,
|
||||
color: RGB8,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct KeyFrame {
|
||||
duration: u64,
|
||||
settings: Vec<LigthSetting>
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RenderAnimation {
|
||||
key_frames: Vec<KeyFrame>,
|
||||
priority: u32,
|
||||
name: String,
|
||||
|
||||
current_frame: usize,
|
||||
start_time: u64,
|
||||
reset: bool,
|
||||
}
|
||||
|
||||
pub struct Render {
|
||||
conn: DBPool,
|
||||
num_leds: u32,
|
||||
|
||||
ws: Option<Ws2812Rpi>,
|
||||
last: Vec<RGB8>,
|
||||
|
||||
animations: Vec<RenderAnimation>,
|
||||
|
||||
local_led_config: Vec<LocalLedConfiguration>,
|
||||
}
|
||||
|
||||
fn try_into_usize (input: Option<u32>) -> Option<usize> {
|
||||
if input.is_none() {return None;}
|
||||
|
||||
let t: Result<usize, _> = input.unwrap().try_into();
|
||||
|
||||
if t.is_err() {return None;}
|
||||
|
||||
return t.ok();
|
||||
}
|
||||
|
||||
impl Render {
|
||||
pub fn new(conn: DBPool) -> Result<Render, Box<dyn Error>> {
|
||||
let mut render = Render {
|
||||
conn,
|
||||
num_leds: 0,
|
||||
ws: None,
|
||||
animations: vec![],
|
||||
local_led_config: vec![],
|
||||
last: vec![],
|
||||
};
|
||||
|
||||
render.load_config()?;
|
||||
|
||||
return Ok(render);
|
||||
}
|
||||
|
||||
pub fn load_config(&mut self) -> Result<(), Box<dyn Error>> {
|
||||
println!("Load config");
|
||||
// GPIO Pin 10 is SPI
|
||||
// Other modes and PINs are available depending on the Raspberry Pi revision
|
||||
// Additional OS configuration might be needed for any mode.
|
||||
// Check https://github.com/jgarff/rpi_ws281x for more information.
|
||||
const PIN: i32 = 18;
|
||||
|
||||
let conn = self.conn.get()?;
|
||||
|
||||
let stmt = conn.prepare("SELECT ledcount, tags from configuration;");
|
||||
|
||||
if stmt.is_err() || stmt.as_ref().ok().is_none() {
|
||||
Err(format!("Something went wrong while reading the database {:?}", stmt.as_ref().err()))?;
|
||||
}
|
||||
|
||||
let mut stmt = stmt?;
|
||||
|
||||
struct Data {
|
||||
ledcount: u32,
|
||||
tag: String
|
||||
}
|
||||
|
||||
let version_iter = stmt.query_map([], |row| {
|
||||
Ok(Data {
|
||||
ledcount: row.get(0)?,
|
||||
tag: row.get(1)?
|
||||
})
|
||||
});
|
||||
|
||||
if version_iter.is_err() || version_iter.as_ref().ok().is_none() {
|
||||
Err(format!("Something went wrong while reading the database {:?}", version_iter.as_ref().err()))?;
|
||||
}
|
||||
|
||||
let version_iter = version_iter.ok().unwrap();
|
||||
|
||||
let mut current_pos = 0;
|
||||
|
||||
let mut local_led_config = Vec::new();
|
||||
|
||||
for data in version_iter {
|
||||
|
||||
let data = data?;
|
||||
|
||||
let tag = if data.tag.is_empty() { None } else { Some(data.tag) };
|
||||
|
||||
let start = current_pos;
|
||||
let end = current_pos + data.ledcount;
|
||||
current_pos = end;
|
||||
|
||||
let start: Result<usize, _> = start.try_into();
|
||||
let end: Result<usize, _> = end.try_into();
|
||||
|
||||
if start.is_err() || start.ok().is_none() || end.is_err() || end.ok().is_none() {
|
||||
println!("Skiping loading configuration");
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = start.ok().unwrap();
|
||||
let end = end.ok().unwrap();
|
||||
|
||||
local_led_config.push(LocalLedConfiguration {
|
||||
tag,
|
||||
start,
|
||||
end
|
||||
});
|
||||
}
|
||||
|
||||
self.num_leds = current_pos;
|
||||
println!("Loaded: {} leds, with config: {:?}", self.num_leds, local_led_config);
|
||||
self.local_led_config = local_led_config;
|
||||
|
||||
self.ws = Some(Ws2812Rpi::new(current_pos as i32, PIN).unwrap());
|
||||
|
||||
println!("Here");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_animation(&mut self, animation: Animation) {
|
||||
|
||||
println!("Added animation");
|
||||
|
||||
let mut key_frames = Vec::new();
|
||||
|
||||
for frame in animation.get_frames().iter() {
|
||||
let mut light_settings = Vec::new();
|
||||
|
||||
for setting in frame.get_settings() {
|
||||
light_settings.push(LigthSetting {
|
||||
tags: setting.get_tags(),
|
||||
start: try_into_usize(setting.get_start()),
|
||||
end: try_into_usize(setting.get_end()),
|
||||
color: RGB8::new(setting.get_r(), setting.get_g(), setting.get_b()),
|
||||
});
|
||||
}
|
||||
|
||||
key_frames.push(KeyFrame { duration: frame.get_duration().into(), settings: light_settings })
|
||||
}
|
||||
|
||||
let new_animation = RenderAnimation {
|
||||
key_frames,
|
||||
priority: animation.get_priority(),
|
||||
name: animation.get_name(),
|
||||
current_frame: 0,
|
||||
start_time: 0,
|
||||
reset: animation.get_repeat(),
|
||||
};
|
||||
|
||||
self.animations.push(new_animation);
|
||||
|
||||
self.animations.sort_by(|a,b| a.priority.partial_cmp(&b.priority).unwrap())
|
||||
}
|
||||
|
||||
pub fn blank(&mut self) {
|
||||
self.animations = Vec::new();
|
||||
}
|
||||
|
||||
pub fn remove_animation(&mut self, animation_name: String) {
|
||||
self.animations = self.animations.clone().into_iter().filter(|ani| !ani.name.eq(&animation_name)).collect::<Vec<RenderAnimation>>();
|
||||
}
|
||||
|
||||
pub fn proccess_settings (&mut self, data: &mut Vec<RGB8>, settings: &Vec<LigthSetting>) {
|
||||
fn load_led_into_light (data: &mut Vec<RGB8>, color: RGB8, start: usize, end: usize) {
|
||||
for i in start..=end {
|
||||
data[i] = color;
|
||||
}
|
||||
}
|
||||
|
||||
for setting in settings {
|
||||
if setting.tags.is_some() {
|
||||
let tags = setting.tags.as_ref().unwrap().to_string();
|
||||
let tags = tags.split(",");
|
||||
let tags_fn = |t: String| tags.clone().into_iter().any(|tag| tag.eq(&t));
|
||||
for config in self.local_led_config.clone() {
|
||||
if config.tag.is_some() && tags_fn(config.tag.unwrap()) {
|
||||
load_led_into_light(data, setting.color, config.start, config.end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if setting.start.is_some() && setting.end.is_some() {
|
||||
load_led_into_light(data, setting.color, setting.start.unwrap(), setting.end.unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self) {
|
||||
|
||||
if self.ws.is_none() { return; }
|
||||
|
||||
let mut data = vec![RGB8::default(); self.num_leds.try_into().unwrap()];
|
||||
|
||||
let time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs();
|
||||
|
||||
let mut to_clean: Vec<String> = Vec::new();
|
||||
|
||||
let animations = self.animations.clone().iter_mut().map(|animation| {
|
||||
let mut animation = animation.to_owned();
|
||||
|
||||
let mut start_time = animation.start_time;
|
||||
|
||||
if start_time == 0 {
|
||||
animation.start_time = time;
|
||||
animation.current_frame = 0;
|
||||
|
||||
self.proccess_settings(&mut data, &animation.key_frames[0].settings);
|
||||
return animation;
|
||||
}
|
||||
|
||||
while start_time + animation.key_frames[animation.current_frame].duration < time {
|
||||
start_time += animation.key_frames[animation.current_frame].duration;
|
||||
animation.start_time = start_time;
|
||||
|
||||
if animation.current_frame + 1 >= animation.key_frames.len() {
|
||||
if animation.reset {
|
||||
animation.start_time = time;
|
||||
animation.current_frame = 0;
|
||||
self.proccess_settings(&mut data, &animation.key_frames[0].settings);
|
||||
} else {
|
||||
to_clean.push(animation.name.to_string());
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
|
||||
animation.current_frame += 1;
|
||||
}
|
||||
|
||||
let cf = animation.current_frame;
|
||||
|
||||
self.proccess_settings(&mut data, &animation.key_frames[cf].settings);
|
||||
|
||||
return animation;
|
||||
}).collect();
|
||||
|
||||
self.animations = animations;
|
||||
|
||||
to_clean.iter().for_each(|i| self.remove_animation(i.to_string()));
|
||||
|
||||
if self.last.is_empty() || !self.last.clone().iter().eq(data.clone().iter()) {
|
||||
let err = self.ws.as_mut().unwrap().write(data.clone().into_iter());
|
||||
|
||||
if err.is_err() || err.ok().is_none() {
|
||||
println!("Failed to write data");
|
||||
}
|
||||
|
||||
self.last = data;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
11
src/utils.rs
Normal file
11
src/utils.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use rocket::{serde::{Deserialize, Serialize}, Responder, http::{ContentType, Header}};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(crate="rocket::serde")]
|
||||
pub struct ApiResponse {
|
||||
pub code: u32,
|
||||
pub message: String
|
||||
}
|
||||
|
||||
#[derive(Responder)]
|
||||
pub struct ApiResponder(String);
|
Loading…
Reference in New Issue
Block a user