Skip to content

Commit c85853f

Browse files
alixinnevtavernier
authored andcommitted
feat: add python feature gate
Disabling the python features removes the dependency on Python libraries. Effects defined using a Python script will not be loaded if this feature is disabled. Fixes #2.
1 parent b62329e commit c85853f

File tree

11 files changed

+333
-151
lines changed

11 files changed

+333
-151
lines changed

Cargo.toml

+6-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ parse-display = "0.5"
4040
paw = "1.0"
4141
pnet = "0.28"
4242
prost = "0.7"
43-
pyo3 = "0.14"
44-
pythonize = "0.14"
43+
pyo3 = { version = "0.14", optional = true }
44+
pythonize = { version = "0.14", optional = true }
4545
regex = "1.5"
4646
serde = "1.0"
4747
serde_derive = "1.0"
@@ -71,3 +71,7 @@ prost-build = "0.7"
7171
[dev-dependencies]
7272
criterion = "0.3"
7373
rand = "0.8"
74+
75+
[features]
76+
default = ["python"]
77+
python = ["pyo3", "pythonize"]

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ Currently implemented features:
3030
* JSON, Protobuf, Flatbuffers and Boblight server
3131
* Black border detector, color channel adjustments, smoothing
3232
* Basic effect support (only setColor and setImage, no custom smoothing, no
33-
per-instance effect directory)
33+
per-instance effect directory). Can be disabled if Python is not available
34+
for the target platform (see the `python` feature).
3435

3536
Extra features not available in hyperion.ng:
3637

src/effects.rs

+136-44
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,14 @@ use crate::{global::InputSourceError, image::RawImage, models::Color};
1111
mod definition;
1212
pub use definition::*;
1313

14-
mod runtime;
14+
mod providers;
15+
pub use providers::Providers;
1516

1617
mod instance;
1718
use instance::*;
1819

20+
use self::providers::{Provider, ProviderError};
21+
1922
pub struct EffectRunHandle {
2023
ctx: Sender<ControlMessage>,
2124
join_handle: Option<JoinHandle<()>>,
@@ -69,52 +72,141 @@ pub enum EffectMessageKind {
6972
SetColor { color: Color },
7073
SetImage { image: Arc<RawImage> },
7174
SetLedColors { colors: Arc<Vec<Color>> },
72-
Completed { result: Result<(), pyo3::PyErr> },
75+
Completed { result: Result<(), ProviderError> },
76+
}
77+
78+
#[derive(Default, Debug, Clone)]
79+
pub struct EffectRegistry {
80+
effects: Vec<EffectHandle>,
7381
}
7482

75-
pub fn run<X: std::fmt::Debug + Clone + Send + 'static>(
76-
effect: &EffectDefinition,
77-
args: serde_json::Value,
78-
led_count: usize,
79-
duration: Option<chrono::Duration>,
80-
priority: i32,
81-
tx: Sender<EffectMessage<X>>,
82-
extra: X,
83-
) -> Result<EffectRunHandle, RunEffectError> {
84-
// Resolve path
85-
let full_path = effect.script_path()?;
86-
87-
// Create control channel
88-
let (ctx, crx) = channel(1);
89-
90-
// Create instance methods
91-
let methods = InstanceMethods::new(
92-
tx.clone(),
93-
crx,
94-
led_count,
95-
duration.and_then(|d| d.to_std().ok()),
96-
extra.clone(),
97-
);
98-
99-
// Run effect
100-
let join_handle = tokio::task::spawn(async move {
101-
// Run the blocking task
102-
let result = tokio::task::spawn_blocking(move || runtime::run(&full_path, args, methods))
83+
impl EffectRegistry {
84+
pub fn new() -> Self {
85+
Self::default()
86+
}
87+
88+
pub fn iter(&self) -> impl Iterator<Item = &EffectDefinition> {
89+
self.effects.iter().map(|handle| &handle.definition)
90+
}
91+
92+
pub fn find_effect(&self, name: &str) -> Option<&EffectHandle> {
93+
self.effects.iter().find(|e| e.definition.name == name)
94+
}
95+
96+
pub fn len(&self) -> usize {
97+
self.effects.len()
98+
}
99+
100+
/// Add definitions to this registry
101+
///
102+
/// # Parameters
103+
///
104+
/// * `providers`: effect providers
105+
/// * `definitions`: effect definitions to register
106+
///
107+
/// # Returns
108+
///
109+
/// Effect definitions that are not supported by any provider.
110+
pub fn add_definitions(
111+
&mut self,
112+
providers: &Providers,
113+
definitions: Vec<EffectDefinition>,
114+
) -> Vec<EffectDefinition> {
115+
let mut remaining = vec![];
116+
117+
for definition in definitions {
118+
if let Some(provider) = providers.get(&definition.script) {
119+
debug!(provider=?provider, effect=%definition.name, "assigned provider to effect");
120+
121+
self.effects.push(EffectHandle {
122+
definition,
123+
provider,
124+
});
125+
} else {
126+
debug!(effect=%definition.name, "no provider for effect");
127+
128+
remaining.push(definition);
129+
}
130+
}
131+
132+
remaining
133+
}
134+
}
135+
136+
#[derive(Debug, Clone)]
137+
pub struct EffectHandle {
138+
pub definition: EffectDefinition,
139+
provider: Arc<dyn Provider>,
140+
}
141+
142+
impl EffectHandle {
143+
pub fn run<X: std::fmt::Debug + Clone + Send + 'static>(
144+
&self,
145+
args: serde_json::Value,
146+
led_count: usize,
147+
duration: Option<chrono::Duration>,
148+
priority: i32,
149+
tx: Sender<EffectMessage<X>>,
150+
extra: X,
151+
) -> Result<EffectRunHandle, RunEffectError> {
152+
// Resolve path
153+
let full_path = self.definition.script_path()?;
154+
155+
// Clone provider arc
156+
let provider = self.provider.clone();
157+
158+
// Create control channel
159+
let (ctx, crx) = channel(1);
160+
161+
// Create channel to wrap data
162+
let (etx, mut erx) = channel(1);
163+
164+
// Create instance methods
165+
let methods =
166+
InstanceMethods::new(etx, crx, led_count, duration.and_then(|d| d.to_std().ok()));
167+
168+
// Run effect
169+
let join_handle = tokio::task::spawn(async move {
170+
// Create the blocking task
171+
let mut run_effect =
172+
tokio::task::spawn_blocking(move || provider.run(&full_path, args, methods));
173+
174+
// Join the blocking task while forwarding the effect messages
175+
let result = loop {
176+
tokio::select! {
177+
kind = erx.recv() => {
178+
if let Some(kind) = kind {
179+
// Add the extra marker to the message and forward it to the instance
180+
let msg = EffectMessage { kind, extra: extra.clone() };
181+
182+
if let Err(err) = tx.send(msg).await {
183+
// This would happen if the effect is running and the instance has
184+
// already shutdown.
185+
error!(err=%err, "failed to forward effect message");
186+
return;
187+
}
188+
}
189+
}
190+
result = &mut run_effect => {
191+
// Unwrap blocking result
192+
break result.expect("failed to await blocking task");
193+
}
194+
}
195+
};
196+
197+
// Send the completion, ignoring failures in case we're shutting down
198+
tx.send(EffectMessage {
199+
kind: EffectMessageKind::Completed { result },
200+
extra,
201+
})
103202
.await
104-
.expect("failed to await blocking task");
203+
.ok();
204+
});
105205

106-
// Send the completion, ignoring failures in case we're shutting down
107-
tx.send(EffectMessage {
108-
kind: EffectMessageKind::Completed { result },
109-
extra,
206+
Ok(EffectRunHandle {
207+
ctx,
208+
join_handle: join_handle.into(),
209+
priority,
110210
})
111-
.await
112-
.ok();
113-
});
114-
115-
Ok(EffectRunHandle {
116-
ctx,
117-
join_handle: join_handle.into(),
118-
priority,
119-
})
211+
}
120212
}

src/effects/instance.rs

+45-27
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,42 @@ use std::{
33
time::{Duration, Instant},
44
};
55

6+
use thiserror::Error;
67
use tokio::sync::mpsc::{Receiver, Sender};
78

8-
use crate::image::RawImage;
9-
10-
use super::{
11-
runtime::{RuntimeMethodError, RuntimeMethods},
12-
EffectMessage, EffectMessageKind,
9+
use crate::{
10+
image::{RawImage, RawImageError},
11+
models::Color,
1312
};
1413

14+
use super::EffectMessageKind;
15+
1516
#[derive(Debug, Clone, Copy, PartialEq)]
1617
pub enum ControlMessage {
1718
Abort,
1819
}
1920

20-
pub struct InstanceMethods<X> {
21-
tx: Sender<EffectMessage<X>>,
21+
pub struct InstanceMethods {
22+
tx: Sender<EffectMessageKind>,
2223
crx: RefCell<Receiver<ControlMessage>>,
2324
led_count: usize,
2425
deadline: Option<Instant>,
2526
aborted: Cell<bool>,
26-
extra: X,
2727
}
2828

29-
impl<X> InstanceMethods<X> {
29+
impl InstanceMethods {
3030
pub fn new(
31-
tx: Sender<EffectMessage<X>>,
31+
tx: Sender<EffectMessageKind>,
3232
crx: Receiver<ControlMessage>,
3333
led_count: usize,
3434
duration: Option<Duration>,
35-
extra: X,
3635
) -> Self {
3736
Self {
3837
tx,
3938
crx: crx.into(),
4039
led_count,
4140
deadline: duration.map(|d| Instant::now() + d),
4241
aborted: false.into(),
43-
extra,
4442
}
4543
}
4644

@@ -93,7 +91,7 @@ impl<X> InstanceMethods<X> {
9391
}
9492
}
9593

96-
impl<X: std::fmt::Debug + Clone> RuntimeMethods for InstanceMethods<X> {
94+
impl RuntimeMethods for InstanceMethods {
9795
fn get_led_count(&self) -> usize {
9896
self.led_count
9997
}
@@ -105,31 +103,51 @@ impl<X: std::fmt::Debug + Clone> RuntimeMethods for InstanceMethods<X> {
105103
fn set_color(&self, color: crate::models::Color) -> Result<(), RuntimeMethodError> {
106104
self.poll_control()?;
107105

108-
self.wrap_result(self.tx.blocking_send(EffectMessage {
109-
kind: EffectMessageKind::SetColor { color },
110-
extra: self.extra.clone(),
111-
}))
106+
self.wrap_result(self.tx.blocking_send(EffectMessageKind::SetColor { color }))
112107
}
113108

114109
fn set_led_colors(&self, colors: Vec<crate::models::Color>) -> Result<(), RuntimeMethodError> {
115110
self.poll_control()?;
116111

117-
self.wrap_result(self.tx.blocking_send(EffectMessage {
118-
kind: EffectMessageKind::SetLedColors {
119-
colors: colors.into(),
120-
},
121-
extra: self.extra.clone(),
112+
self.wrap_result(self.tx.blocking_send(EffectMessageKind::SetLedColors {
113+
colors: colors.into(),
122114
}))
123115
}
124116

125117
fn set_image(&self, image: RawImage) -> Result<(), RuntimeMethodError> {
126118
self.poll_control()?;
127119

128-
self.wrap_result(self.tx.blocking_send(EffectMessage {
129-
kind: EffectMessageKind::SetImage {
130-
image: image.into(),
131-
},
132-
extra: self.extra.clone(),
120+
self.wrap_result(self.tx.blocking_send(EffectMessageKind::SetImage {
121+
image: image.into(),
133122
}))
134123
}
135124
}
125+
126+
pub trait RuntimeMethods {
127+
fn get_led_count(&self) -> usize;
128+
fn abort(&self) -> bool;
129+
130+
fn set_color(&self, color: Color) -> Result<(), RuntimeMethodError>;
131+
fn set_led_colors(&self, colors: Vec<Color>) -> Result<(), RuntimeMethodError>;
132+
fn set_image(&self, image: RawImage) -> Result<(), RuntimeMethodError>;
133+
}
134+
135+
#[derive(Debug, Error)]
136+
pub enum RuntimeMethodError {
137+
#[cfg(feature = "python")]
138+
#[error("Invalid arguments to hyperion.{name}")]
139+
InvalidArguments { name: &'static str },
140+
#[cfg(feature = "python")]
141+
#[error("Length of bytearray argument should be 3*ledCount")]
142+
InvalidByteArray,
143+
#[error("Effect aborted")]
144+
EffectAborted,
145+
#[error(transparent)]
146+
InvalidImageData(#[from] RawImageError),
147+
}
148+
149+
impl<T> From<tokio::sync::mpsc::error::SendError<T>> for RuntimeMethodError {
150+
fn from(_: tokio::sync::mpsc::error::SendError<T>) -> Self {
151+
Self::EffectAborted
152+
}
153+
}

0 commit comments

Comments
 (0)