Skip to content

Commit

Permalink
Implement plugin discovery
Browse files Browse the repository at this point in the history
closes #141
  • Loading branch information
mrozycki committed May 19, 2023
1 parent 3a0b8d0 commit b6e0c92
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 113 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUSTMAS_TTY_PATH=/dev/ttyACM0
RUSTMAS_DB_PATH=webapi/db.sqlite

# Where the compiled plugins are
RUSTMAS_PLUGIN_DIR=target/release
RUSTMAS_PLUGIN_DIR=plugins

# Logging configuration
RUST_LOG=info
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/target
/plugins
**/captures

*.csv
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

4 changes: 0 additions & 4 deletions animations/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,6 @@ name = "animations"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "blank"
path = "src/blank.rs"

[[bin]]
name = "barber_pole"
path = "src/barber_pole.rs"
Expand Down
30 changes: 0 additions & 30 deletions animations/src/blank.rs

This file was deleted.

1 change: 1 addition & 0 deletions animator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ animation-api = { path = "../animation-api" }
csv = "1.1.6"
chrono = "0.4.23"
clap = { version = "4.0.18", features = ["derive"] }
glob = "0.3.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
Expand Down
80 changes: 65 additions & 15 deletions animator/src/factory.rs
Original file line number Diff line number Diff line change
@@ -1,41 +1,91 @@
use std::path::{Path, PathBuf};
use std::{
collections::HashMap,
error::Error,
path::{Path, PathBuf},
};

use log::info;
use serde::Deserialize;

use crate::jsonrpc_animation::{AnimationPlugin, JsonRpcEndpoint};
use crate::jsonrpc_animation::{AnimationPlugin, JsonRpcEndpoint, JsonRpcEndpointError};

#[derive(Deserialize)]
pub struct PluginManifest {
pub display_name: String,
}

pub struct Plugin {
pub manifest: PluginManifest,
pub path: PathBuf,
}

impl Plugin {
fn start(&self) -> Result<JsonRpcEndpoint, JsonRpcEndpointError> {
#[cfg(windows)]
let executable_name = "plugin.exe";

#[cfg(not(windows))]
let executable_name = "plugin";

JsonRpcEndpoint::new(self.path.join(executable_name))
}
}

pub struct AnimationFactory {
plugin_dir: PathBuf,
plugins: HashMap<String, Plugin>,
points: Vec<(f64, f64, f64)>,
}

impl AnimationFactory {
pub fn new<P: AsRef<Path>>(plugin_dir: P, points: Vec<(f64, f64, f64)>) -> Self {
Self {
plugin_dir: plugin_dir.as_ref().to_owned(),
plugins: HashMap::new(),
points,
}
}

#[cfg(windows)]
fn make_path(&self, name: &str) -> PathBuf {
self.plugin_dir.join(Path::new(name).with_extension("exe"))
pub fn discover(&mut self) -> Result<(), Box<dyn Error>> {
self.plugins = glob::glob(&format!(
"{}/*/manifest.json",
self.plugin_dir
.to_str()
.ok_or("Plugins directory path is not valid UTF-8")?
))?
.map(|path| {
path.map_err(|e| -> Box<dyn Error> { Box::new(e) })
.and_then(|path| -> Result<_, Box<dyn Error>> {
let manifest: PluginManifest = serde_json::from_slice(&std::fs::read(&path)?)?;
let path = path.parent().unwrap().to_owned();
let id = path
.file_name()
.unwrap()
.to_str()
.ok_or("Plugin name not valid UTF-8")?
.to_owned();
Ok((id, Plugin { path, manifest }))
})
})
.collect::<Result<_, _>>()?;

Ok(())
}

#[cfg(not(windows))]
fn make_path(&self, name: &str) -> PathBuf {
self.plugin_dir.join(name)
pub fn list(&self) -> &HashMap<String, Plugin> {
&self.plugins
}

pub fn make(&self, name: &str) -> AnimationPlugin {
pub fn make(&self, name: &str) -> Result<AnimationPlugin, Box<dyn Error>> {
let Some(plugin) = self.plugins.get(name) else {
return Err(format!("No plugin with name '{}'", name).into());
};

info!(
"Trying to start plugin app {} from directory {:?}",
name, self.plugin_dir
"Trying to start plugin app '{}' from directory '{:?}'",
name, plugin.path
);

AnimationPlugin::new(
JsonRpcEndpoint::new(self.make_path(name)).unwrap(),
self.points.clone(),
)
Ok(AnimationPlugin::new(plugin.start()?, self.points.clone()))
}
}
85 changes: 58 additions & 27 deletions animator/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
mod factory;
mod jsonrpc_animation;

use std::collections::HashMap;
use std::error::Error;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use chrono::{DateTime, Duration, Utc};
use client::combined::{CombinedLightClient, CombinedLightClientBuilder};
use factory::AnimationFactory;
use factory::{AnimationFactory, Plugin};
use jsonrpc_animation::AnimationPlugin;
use log::{info, warn};
use rustmas_light_client as client;
Expand All @@ -24,7 +25,7 @@ enum ConnectionStatus {
}

pub struct ControllerState {
animation: AnimationPlugin,
animation: Option<AnimationPlugin>,
last_frame: DateTime<Utc>,
next_frame: DateTime<Utc>,
fps: f64,
Expand All @@ -42,16 +43,15 @@ impl Controller {
plugin_dir: P,
client: Box<dyn rustmas_light_client::LightClient + Sync + Send>,
) -> Self {
let animation_factory = AnimationFactory::new(plugin_dir, points);
let animation = animation_factory.make("blank");
let now = Utc::now();
let state = Arc::new(Mutex::new(ControllerState {
last_frame: now,
next_frame: now,
fps: animation.get_fps(),
animation,
fps: 0.0,
animation: None,
}));
let join_handle = tokio::spawn(Self::run(state.clone(), client));
let join_handle = tokio::spawn(Self::run(state.clone(), client, points.len()));
let animation_factory = AnimationFactory::new(plugin_dir, points);

Self {
state,
Expand All @@ -63,6 +63,7 @@ impl Controller {
async fn run(
state: Arc<Mutex<ControllerState>>,
client: Box<dyn rustmas_light_client::LightClient + Sync + Send>,
point_count: usize,
) {
let start_backoff_delay: Duration = Duration::seconds(1);
let max_backoff_delay: Duration = Duration::seconds(8);
Expand Down Expand Up @@ -95,11 +96,13 @@ impl Controller {
};

let delta = now - state.last_frame;
state
.animation
.update(delta.num_milliseconds() as f64 / 1000.0);
state.last_frame = now;
state.animation.render()
if let Some(ref mut animation) = state.animation {
animation.update(delta.num_milliseconds() as f64 / 1000.0);
animation.render()
} else {
lightfx::Frame::new_black(point_count)
}
};

match client.display_frame(&frame).await {
Expand Down Expand Up @@ -147,51 +150,71 @@ impl Controller {
pub async fn switch_animation(&self, name: &str) -> Result<(), Box<dyn Error>> {
info!("Trying to switch animation to \"{}\"", name);
let mut state = self.state.lock().await;
state.animation = self.animation_factory.make(name);
let new_animation = self.animation_factory.make(name)?;

let now = Utc::now();
let new_fps = state.animation.get_fps();
state.fps = new_fps;
state.fps = new_animation.get_fps();
state.last_frame = now;
state.next_frame = now;
state.animation = Some(new_animation);
Ok(())
}

pub async fn reload_animation(&self) -> Result<(), Box<dyn Error>> {
let mut state = self.state.lock().await;
let name = state.animation.animation_name();
let Some(name) = state.animation.as_ref().map(AnimationPlugin::animation_name) else { return Ok(()) };

info!("Reloading animation \"{}\"", name);
state.animation = self.animation_factory.make(&name);
let new_animation = self.animation_factory.make(&name)?;

let now = Utc::now();
let new_fps = state.animation.get_fps();
state.fps = new_fps;
state.fps = new_animation.get_fps();
state.last_frame = now;
state.next_frame = now;
state.animation = Some(new_animation);
Ok(())
}

pub async fn turn_off(&self) {
info!("Turning off the animation");
let mut state = self.state.lock().await;

let now = Utc::now();
state.fps = 0.0;
state.last_frame = now;
state.next_frame = now;
state.animation = None;
}

pub async fn parameters(&self) -> serde_json::Value {
let animation = &self.state.lock().await.animation;
json!({
"name": animation.animation_name(),
"schema": animation.parameter_schema(),
"values": animation.get_parameters(),
})
if let Some(animation) = &self.state.lock().await.animation {
json!({
"name": animation.animation_name(),
"schema": animation.parameter_schema(),
"values": animation.get_parameters(),
})
} else {
json!({})
}
}

pub async fn parameter_values(&self) -> serde_json::Value {
self.state.lock().await.animation.get_parameters()
if let Some(animation) = &self.state.lock().await.animation {
animation.get_parameters()
} else {
json!({})
}
}

pub async fn set_parameters(
&mut self,
parameters: serde_json::Value,
) -> Result<(), Box<dyn Error>> {
let mut state = self.state.lock().await;
state.animation.set_parameters(parameters)?;
state.next_frame = Utc::now();
if let Some(ref mut animation) = state.animation {
animation.set_parameters(parameters)?;
state.next_frame = Utc::now();
}
Ok(())
}

Expand All @@ -200,6 +223,14 @@ impl Controller {

Ok(())
}

pub fn discover_animations(&mut self) -> Result<(), Box<dyn Error>> {
self.animation_factory.discover()
}

pub fn list_animations(&self) -> &HashMap<String, Plugin> {
self.animation_factory.list()
}
}

pub struct ControllerBuilder {
Expand Down
Loading

0 comments on commit b6e0c92

Please sign in to comment.