Complete proof of concept
This commit is contained in:
parent
0435f14680
commit
569eab5880
46 changed files with 1413 additions and 169 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -42,8 +42,8 @@ yalc.lock
|
|||
.vscode/settings.json
|
||||
|
||||
# ignore Rust compiler files
|
||||
/server/target
|
||||
backend
|
||||
/backend/target
|
||||
/backend/out
|
||||
/bin
|
||||
|
||||
# packaged teasers
|
||||
|
|
33
server/Cargo.lock → backend/Cargo.lock
generated
33
server/Cargo.lock → backend/Cargo.lock
generated
|
@ -2,6 +2,28 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
|
@ -349,10 +371,12 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
|||
name = "kaylon"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simplelog",
|
||||
"tokio",
|
||||
"usdpl-back",
|
||||
]
|
||||
|
||||
|
@ -959,11 +983,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "usdpl-back"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cbbc0781e83ba990f8239142e33173a2d2548701775f3db66702d1af4fd0319a"
|
||||
version = "0.7.0"
|
||||
dependencies = [
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"log",
|
||||
"tokio",
|
||||
"usdpl-core",
|
||||
"warp",
|
||||
|
@ -972,8 +997,6 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "usdpl-core"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "862153581fac266458521f49e5906a71c1eee1665cb4c7d71e9586bd34b45394"
|
||||
dependencies = [
|
||||
"base64",
|
||||
]
|
|
@ -6,8 +6,13 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
usdpl-back = { version = "0.6.0", features = ["decky"] }
|
||||
usdpl-back = { version = "0.7.0", features = ["decky"], path = "../../usdpl-rs/usdpl-back" }
|
||||
|
||||
# async
|
||||
tokio = { version = "*", features = ["sync", "time"] }
|
||||
async-trait = "0.1.57"
|
||||
|
||||
# json
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
12
backend/build.sh
Executable file
12
backend/build.sh
Executable file
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
|
||||
#cargo build --release --target x86_64-unknown-linux-musl
|
||||
cargo build --target x86_64-unknown-linux-musl
|
||||
#cross build --release
|
||||
|
||||
mkdir -p ../bin
|
||||
#cp ./target/x86_64-unknown-linux-musl/release/kaylon ../bin/backend
|
||||
cp ./target/x86_64-unknown-linux-musl/debug/kaylon ../bin/backend
|
||||
#cp ./target/release/kaylon ../bin/backend
|
||||
|
||||
cp ../kaylon.json ../bin/kaylon.json
|
25
backend/src/api/about.rs
Normal file
25
backend/src/api/about.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use std::sync::{Mutex, mpsc::{Sender, channel}};
|
||||
|
||||
use super::ApiParameterType;
|
||||
|
||||
use crate::runtime::{QueueAction, QueueItem};
|
||||
|
||||
pub fn get_about(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
|
||||
let sender = Mutex::new(sender);
|
||||
move |_| {
|
||||
log::debug!("API: get_about");
|
||||
let (rx, tx) = channel();
|
||||
sender.lock().unwrap().send(
|
||||
QueueItem {
|
||||
action: QueueAction::GetAbout {
|
||||
respond_to: rx,
|
||||
}
|
||||
}
|
||||
).unwrap();
|
||||
vec![
|
||||
usdpl_back::core::serdes::Primitive::Json(
|
||||
serde_json::to_string(&tx.recv().unwrap()).unwrap()
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
72
backend/src/api/get_display.rs
Normal file
72
backend/src/api/get_display.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
use std::sync::{Mutex, mpsc::{Sender, channel, self}};
|
||||
|
||||
use usdpl_back::core::serdes::Primitive;
|
||||
use usdpl_back::AsyncCallable;
|
||||
|
||||
use super::ApiParameterType;
|
||||
|
||||
use crate::runtime::{QueueAction, QueueItem};
|
||||
|
||||
pub struct GetDisplayEndpoint {
|
||||
//sender: tokio::sync::mpsc::Sender<SetCallbackAsync>,
|
||||
//receiver: Mutex<Option<tokio::sync::mpsc::Receiver<SetCallbackAsync>>>,
|
||||
sync_sender: Mutex<Sender<QueueItem>>,
|
||||
}
|
||||
|
||||
impl GetDisplayEndpoint {
|
||||
pub fn new(sender: Sender<QueueItem>) -> Self {
|
||||
//let (async_tx, async_rx) = tokio::sync::mpsc::channel::<SetCallbackAsync>(64);
|
||||
Self {
|
||||
//sender: async_tx,
|
||||
//receiver: Mutex::new(Some(async_rx)),
|
||||
sync_sender: Mutex::new(sender),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl AsyncCallable for GetDisplayEndpoint {
|
||||
async fn call(&self, params: ApiParameterType) -> ApiParameterType {
|
||||
log::debug!("API: get_display");
|
||||
if let Some(Primitive::F64(index)) = params.get(0) {
|
||||
let index = *index as usize;
|
||||
let (respond_to, receiver) = channel();
|
||||
log::info!("requesting display for item #{}", index);
|
||||
let send_result = self.sync_sender.lock().unwrap().send(
|
||||
QueueItem {
|
||||
action: QueueAction::SetCallback {
|
||||
index,
|
||||
respond_to,
|
||||
}
|
||||
}
|
||||
);
|
||||
if let Ok(_) = send_result {
|
||||
// TODO: don't poll for response
|
||||
log::info!("waiting for display for item #{}", index);
|
||||
let sleep_duration = std::time::Duration::from_millis(10);
|
||||
let receiver = Mutex::new(receiver);
|
||||
loop {
|
||||
let received = receiver.lock().unwrap().try_recv();
|
||||
match received {
|
||||
Err(mpsc::TryRecvError::Disconnected) => {
|
||||
log::info!("Failed to response for get_display for #{}", index);
|
||||
return vec![Primitive::Empty];
|
||||
},
|
||||
Err(_) => {},
|
||||
Ok(x) => {
|
||||
log::debug!("got display for item #{}", index);
|
||||
return vec![x];
|
||||
},
|
||||
}
|
||||
tokio::time::sleep(sleep_duration).await;
|
||||
}
|
||||
} else {
|
||||
log::info!("Failed to get_display for #{}", index);
|
||||
vec![Primitive::Empty]
|
||||
}
|
||||
|
||||
} else {
|
||||
vec![Primitive::Empty]
|
||||
}
|
||||
}
|
||||
}
|
26
backend/src/api/get_item.rs
Normal file
26
backend/src/api/get_item.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use std::sync::{Mutex, mpsc::{Sender, channel}};
|
||||
|
||||
use super::ApiParameterType;
|
||||
|
||||
use crate::runtime::{QueueAction, QueueItem};
|
||||
|
||||
pub fn get_items(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
|
||||
let sender = Mutex::new(sender);
|
||||
move |_| {
|
||||
log::debug!("API: get_items");
|
||||
let (rx, tx) = channel();
|
||||
sender.lock().unwrap().send(
|
||||
QueueItem {
|
||||
action: QueueAction::DoReload {
|
||||
respond_to: rx,
|
||||
}
|
||||
}
|
||||
).unwrap();
|
||||
log::info!("waiting for items");
|
||||
vec![
|
||||
usdpl_back::core::serdes::Primitive::Json(
|
||||
serde_json::to_string(&tx.recv().unwrap()).unwrap()
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
13
backend/src/api/mod.rs
Normal file
13
backend/src/api/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
mod about;
|
||||
mod get_display;
|
||||
mod get_item;
|
||||
mod on_update;
|
||||
mod reload;
|
||||
|
||||
pub use about::get_about;
|
||||
pub use get_display::GetDisplayEndpoint;
|
||||
pub use get_item::get_items;
|
||||
pub use on_update::on_update;
|
||||
pub use reload::reload;
|
||||
|
||||
pub(super) type ApiParameterType = Vec<usdpl_back::core::serdes::Primitive>;
|
34
backend/src/api/on_update.rs
Normal file
34
backend/src/api/on_update.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
use std::sync::{Mutex, mpsc::Sender};
|
||||
|
||||
use usdpl_back::core::serdes::Primitive;
|
||||
|
||||
use super::ApiParameterType;
|
||||
|
||||
use crate::runtime::{QueueAction, QueueItem};
|
||||
|
||||
pub fn on_update(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
|
||||
let sender = Mutex::new(sender);
|
||||
move |mut params: ApiParameterType| {
|
||||
log::debug!("API: on_update");
|
||||
if params.len() == 2 {
|
||||
if let Primitive::F64(index) = params.remove(0) {
|
||||
let index = index as usize;
|
||||
let val = params.remove(0);
|
||||
sender.lock().unwrap().send(
|
||||
QueueItem {
|
||||
action: QueueAction::DoUpdate {
|
||||
index,
|
||||
value: val,
|
||||
}
|
||||
}
|
||||
).unwrap();
|
||||
log::info!("Sent update for #{}", index);
|
||||
vec![true.into()]
|
||||
} else {
|
||||
vec![false.into()]
|
||||
}
|
||||
} else {
|
||||
vec![false.into()]
|
||||
}
|
||||
}
|
||||
}
|
26
backend/src/api/reload.rs
Normal file
26
backend/src/api/reload.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use std::sync::{Mutex, mpsc::{Sender, channel}};
|
||||
|
||||
use super::ApiParameterType;
|
||||
|
||||
use crate::runtime::{QueueAction, QueueItem};
|
||||
|
||||
pub fn reload(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
|
||||
let sender = Mutex::new(sender);
|
||||
move |_| {
|
||||
log::debug!("API: reload");
|
||||
let (rx, tx) = channel();
|
||||
sender.lock().unwrap().send(
|
||||
QueueItem {
|
||||
action: QueueAction::DoReload {
|
||||
respond_to: rx,
|
||||
}
|
||||
}
|
||||
).unwrap();
|
||||
log::info!("waiting for JSON reload");
|
||||
vec![
|
||||
usdpl_back::core::serdes::Primitive::Json(
|
||||
serde_json::to_string(&tx.recv().unwrap()).unwrap()
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
24
backend/src/config/about.rs
Normal file
24
backend/src/config/about.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct AboutConfig {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: String,
|
||||
pub url: Option<String>,
|
||||
pub authors: Vec<String>,
|
||||
pub license: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for AboutConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: env!("CARGO_PKG_NAME").to_owned(),
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
description: env!("CARGO_PKG_DESCRIPTION").to_owned(),
|
||||
url: Some(env!("CARGO_PKG_HOMEPAGE").to_owned()),
|
||||
authors: env!("CARGO_PKG_AUTHORS").split(':').map(|x| x.to_owned()).collect(),
|
||||
license: Some(env!("CARGO_PKG_LICENSE").to_owned())
|
||||
}
|
||||
}
|
||||
}
|
52
backend/src/config/base.rs
Normal file
52
backend/src/config/base.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use super::{ElementConfig, AboutConfig};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "api-version")]
|
||||
pub enum BaseConfig {
|
||||
#[serde(rename = "v0.0.0")]
|
||||
V0 {
|
||||
items: Vec<ElementConfig>,
|
||||
about: AboutConfig,
|
||||
},
|
||||
}
|
||||
|
||||
impl BaseConfig {
|
||||
pub fn load<P: AsRef<std::path::Path>>(path: P) -> Self {
|
||||
//let path = std::path::Path::new("./").join(path);
|
||||
let path = path.as_ref();
|
||||
match std::fs::File::open(&path) {
|
||||
Ok(file) => {
|
||||
let reader = std::io::BufReader::new(file);
|
||||
match serde_json::from_reader(reader) {
|
||||
Ok(conf) => return conf,
|
||||
Err(e) => log::error!("Failed to deserialize {}: {}", path.display(), e),
|
||||
}
|
||||
},
|
||||
Err(e) => log::error!("Failed to open {}: {}", path.display(), e),
|
||||
}
|
||||
panic!("Cannot open {}", path.display())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_about(&self) -> &AboutConfig {
|
||||
match self {
|
||||
Self::V0 {about, ..} => about,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_item(&self, index: usize) -> Option<&ElementConfig> {
|
||||
match self {
|
||||
Self::V0 {items, ..} => items.get(index),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn items(&self) -> &Vec<ElementConfig> {
|
||||
match self {
|
||||
Self::V0 {items, ..} => items,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use super::{ButtonConfig, ToggleConfig, SliderConfig, ReadingConfig};
|
||||
use super::{ButtonConfig, ToggleConfig, SliderConfig, ReadingConfig, ResultDisplayConfig};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "element")]
|
||||
|
@ -12,5 +12,7 @@ pub enum ElementConfig {
|
|||
#[serde(rename = "slider")]
|
||||
Slider(SliderConfig),
|
||||
#[serde(rename = "reading")]
|
||||
Reading(ReadingConfig),
|
||||
ReadingDisplay(ReadingConfig),
|
||||
#[serde(rename = "result-display")]
|
||||
ResultDisplay(ResultDisplayConfig),
|
||||
}
|
|
@ -4,6 +4,7 @@ mod base;
|
|||
mod button;
|
||||
mod element;
|
||||
mod reading;
|
||||
mod result_display;
|
||||
mod slider;
|
||||
mod toggle;
|
||||
|
||||
|
@ -13,6 +14,7 @@ pub use base::BaseConfig;
|
|||
pub use button::ButtonConfig;
|
||||
pub use element::ElementConfig;
|
||||
pub use reading::ReadingConfig;
|
||||
pub use result_display::ResultDisplayConfig;
|
||||
pub use slider::SliderConfig;
|
||||
pub use toggle::ToggleConfig;
|
||||
|
||||
|
@ -31,8 +33,7 @@ mod test {
|
|||
ElementConfig::Toggle(ToggleConfig {
|
||||
title: "Test Toggle".into(),
|
||||
description: Some("Toggle description".into()),
|
||||
on_enable: ActionConfig::Command(CommandAction{run: "echo 'hello toggle 1'".into()}),
|
||||
on_disable: ActionConfig::Command(CommandAction{run: "echo 'hello toggle 0'".into()}),
|
||||
on_toggle: ActionConfig::Command(CommandAction{run: "echo 'hello toggle $KAYLON_VALUE'".into()}),
|
||||
}),
|
||||
ElementConfig::Slider(SliderConfig {
|
||||
title: "Test Slider".into(),
|
||||
|
@ -41,18 +42,22 @@ mod test {
|
|||
notches: None,
|
||||
on_set: ActionConfig::Command(CommandAction{run: "echo 'hello slider'".into()}),
|
||||
}),
|
||||
ElementConfig::Reading(ReadingConfig {
|
||||
ElementConfig::ReadingDisplay(ReadingConfig {
|
||||
title: "Test Reading".into(),
|
||||
period_ms: 10000,
|
||||
on_period: ActionConfig::Command(CommandAction{run: "echo 'hello reading'".into()})
|
||||
}),
|
||||
ElementConfig::ResultDisplay(ResultDisplayConfig {
|
||||
title: "Test Reading".into(),
|
||||
result_of: 1,
|
||||
}),
|
||||
],
|
||||
about: AboutConfig {
|
||||
name: "Test name".into(),
|
||||
version: "v0.42.0".into(),
|
||||
description: "Test description".into(),
|
||||
url: Some("https://github.com/NGnius/kaylon".into()),
|
||||
author: Some("NGnius <ngniusness@gmail.com>".into()),
|
||||
authors: vec!["NGnius <ngniusness@gmail.com>".into()],
|
||||
license: Some("MIT".into()),
|
||||
},
|
||||
};
|
8
backend/src/config/result_display.rs
Normal file
8
backend/src/config/result_display.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct ResultDisplayConfig {
|
||||
pub title: String,
|
||||
/// index of element who's action's result will be used
|
||||
pub result_of: usize,
|
||||
}
|
|
@ -6,6 +6,5 @@ use super::ActionConfig;
|
|||
pub struct ToggleConfig {
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub on_enable: ActionConfig,
|
||||
pub on_disable: ActionConfig,
|
||||
pub on_toggle: ActionConfig,
|
||||
}
|
6
backend/src/consts.rs
Normal file
6
backend/src/consts.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub const PORT: u16 = 25717;
|
||||
|
||||
pub const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME");
|
||||
pub const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub const FILEPATH: &'static str = "./bin/kaylon.json";
|
34
backend/src/main.rs
Normal file
34
backend/src/main.rs
Normal file
|
@ -0,0 +1,34 @@
|
|||
mod api;
|
||||
mod config;
|
||||
mod consts;
|
||||
mod runtime;
|
||||
|
||||
use simplelog::{WriteLogger, LevelFilter};
|
||||
|
||||
use usdpl_back::Instance;
|
||||
use usdpl_back::core::serdes::Primitive;
|
||||
|
||||
fn main() -> Result<(), ()> {
|
||||
let log_filepath = format!("/tmp/{}.log", consts::PACKAGE_NAME);
|
||||
WriteLogger::init(
|
||||
#[cfg(debug_assertions)]{LevelFilter::Debug},
|
||||
#[cfg(not(debug_assertions))]{LevelFilter::Info},
|
||||
Default::default(),
|
||||
std::fs::File::create(&log_filepath).unwrap()
|
||||
).unwrap();
|
||||
|
||||
let kaylon_conf = config::BaseConfig::load(consts::FILEPATH);
|
||||
let (executor, sender) = runtime::RuntimeExecutor::new(kaylon_conf);
|
||||
|
||||
log::info!("Starting back-end ({} v{})", consts::PACKAGE_NAME, consts::PACKAGE_VERSION);
|
||||
println!("Starting back-end ({} v{})", consts::PACKAGE_NAME, consts::PACKAGE_VERSION);
|
||||
let instance = Instance::new(consts::PORT)
|
||||
.register("hello", |_: Vec<Primitive>| vec![format!("Hello {}", consts::PACKAGE_NAME).into()])
|
||||
.register_blocking("get_about", api::get_about(sender.clone()))
|
||||
.register_async("get_display", api::GetDisplayEndpoint::new(sender.clone()))
|
||||
.register_blocking("get_items", api::get_items(sender.clone()))
|
||||
.register("on_update", api::on_update(sender.clone()))
|
||||
.register_blocking("reload", api::reload(sender.clone()));
|
||||
let _exec_handle = executor.spawn();
|
||||
instance.run_blocking()
|
||||
}
|
68
backend/src/runtime/actor.rs
Normal file
68
backend/src/runtime/actor.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
use usdpl_back::core::serdes::Primitive;
|
||||
|
||||
use crate::config::{ElementConfig, ActionConfig};
|
||||
|
||||
pub type ActError = String;
|
||||
|
||||
pub trait Act: Sized {
|
||||
type Param;
|
||||
type Config: ?Sized;
|
||||
type Return;
|
||||
fn build(config: &Self::Config, parameter: Self::Param) -> Result<Self, ActError>;
|
||||
fn run(self) -> Self::Return;
|
||||
}
|
||||
|
||||
pub struct Actor {
|
||||
actor_type: ActorType,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl Act for Actor {
|
||||
type Param = (usize, Primitive);
|
||||
type Config = ElementConfig;
|
||||
type Return = Primitive;
|
||||
|
||||
fn build(config: &ElementConfig, parameter: Self::Param) -> Result<Self, ActError> {
|
||||
let a_type = match config {
|
||||
ElementConfig::Button(b) => ActorType::build(&b.on_click, parameter.1),
|
||||
ElementConfig::Toggle(t) => ActorType::build(&t.on_toggle, parameter.1),
|
||||
ElementConfig::Slider(s) => ActorType::build(&s.on_set, parameter.1),
|
||||
ElementConfig::ReadingDisplay(r) => ActorType::build(&r.on_period, parameter.1),
|
||||
ElementConfig::ResultDisplay(_) => Err(format!("Item #{} is a ResultDisplay, which can't act", parameter.0)),
|
||||
}?;
|
||||
Ok(Self {
|
||||
actor_type: a_type,
|
||||
index: parameter.0,
|
||||
})
|
||||
}
|
||||
|
||||
fn run(self) -> Self::Return {
|
||||
log::info!("Running act for item {}", self.index);
|
||||
let result = self.actor_type.run();
|
||||
log::info!("Completed act for item {}", self.index);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActorType {
|
||||
Command(super::CommandActor),
|
||||
}
|
||||
|
||||
impl Act for ActorType {
|
||||
type Param = Primitive;
|
||||
type Config = ActionConfig;
|
||||
type Return = Primitive;
|
||||
|
||||
fn build(config: &Self::Config, parameter: Self::Param) -> Result<Self, ActError> {
|
||||
Ok(match config {
|
||||
ActionConfig::Command(c) =>
|
||||
Self::Command(super::CommandActor::build(c, parameter)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn run(self) -> Self::Return {
|
||||
match self {
|
||||
Self::Command(c) => c.run().into(),
|
||||
}
|
||||
}
|
||||
}
|
61
backend/src/runtime/command_actor.rs
Normal file
61
backend/src/runtime/command_actor.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use std::process::Command;
|
||||
|
||||
use usdpl_back::core::serdes::Primitive;
|
||||
|
||||
use crate::config::CommandAction;
|
||||
use super::{Act, ActError};
|
||||
|
||||
const VALUE_ENV_VAR: &str = "KAYLON_VALUE";
|
||||
|
||||
pub struct CommandActor {
|
||||
shell: String,
|
||||
run: String,
|
||||
variable: String,
|
||||
}
|
||||
|
||||
impl CommandActor {
|
||||
fn primitive_to_string(obj: Primitive) -> String {
|
||||
match obj {
|
||||
Primitive::Empty => String::new(),
|
||||
Primitive::String(s) => s,
|
||||
Primitive::F32(f) => f.to_string(),
|
||||
Primitive::F64(f) => f.to_string(),
|
||||
Primitive::I32(i) => i.to_string(),
|
||||
Primitive::I64(i) => i.to_string(),
|
||||
Primitive::U32(u) => u.to_string(),
|
||||
Primitive::U64(u) => u.to_string(),
|
||||
Primitive::Bool(b) => b.to_string().to_uppercase(),
|
||||
Primitive::Json(j) => j,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Act for CommandActor {
|
||||
type Param = Primitive;
|
||||
type Config = CommandAction;
|
||||
type Return = String;
|
||||
|
||||
fn build(config: &CommandAction, parameter: Primitive) -> Result<Self, ActError> {
|
||||
Ok(
|
||||
Self {
|
||||
shell: "bash".to_owned(),
|
||||
run: config.run.clone(),
|
||||
variable: Self::primitive_to_string(parameter),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn run(self) -> Self::Return {
|
||||
let output = Command::new(&self.shell)
|
||||
.args(["-c", &self.run])
|
||||
.env(VALUE_ENV_VAR, &self.variable)
|
||||
.output()
|
||||
.expect(&format!("Cannot run `{}`", &self.run));
|
||||
if !output.stderr.is_empty() {
|
||||
log::error!("Error running `{}`: {}", &self.run, String::from_utf8(output.stderr).unwrap_or_else(|_| "<non utf-8 stderr output>".to_owned()))
|
||||
}
|
||||
let result = String::from_utf8(output.stdout).expect(&format!("Cannot parse stdout from `{}` as UTF-8", self.run));
|
||||
log::debug!("CommandActor ran `{}` (${}=\"{}\") -> `{}`", &self.run, VALUE_ENV_VAR, &self.variable, &result);
|
||||
result
|
||||
}
|
||||
}
|
26
backend/src/runtime/communication.rs
Normal file
26
backend/src/runtime/communication.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use std::sync::mpsc::Sender;
|
||||
|
||||
use usdpl_back::core::serdes::Primitive;
|
||||
|
||||
use crate::config::{AboutConfig, ElementConfig};
|
||||
|
||||
pub enum QueueAction {
|
||||
GetAbout {
|
||||
respond_to: Sender<AboutConfig>,
|
||||
},
|
||||
DoUpdate {
|
||||
index: usize,
|
||||
value: Primitive,
|
||||
},
|
||||
DoReload {
|
||||
respond_to: Sender<Vec<ElementConfig>>
|
||||
},
|
||||
SetCallback {
|
||||
index: usize,
|
||||
respond_to: Sender<Primitive>,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QueueItem {
|
||||
pub action: QueueAction,
|
||||
}
|
121
backend/src/runtime/executor.rs
Normal file
121
backend/src/runtime/executor.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
use std::thread;
|
||||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
use crate::config::{BaseConfig, ElementConfig};
|
||||
use super::{QueueItem, QueueAction, Act};
|
||||
use super::{ResultRouter, RouterCommand};
|
||||
|
||||
pub struct RuntimeExecutor {
|
||||
config_data: BaseConfig,
|
||||
tasks_receiver: Receiver<QueueItem>
|
||||
}
|
||||
|
||||
impl RuntimeExecutor {
|
||||
pub fn new(conf: BaseConfig) -> (Self, Sender<QueueItem>) {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
(Self {
|
||||
config_data: conf,
|
||||
tasks_receiver: rx,
|
||||
}, tx)
|
||||
}
|
||||
|
||||
pub fn spawn(self) -> thread::JoinHandle<()> {
|
||||
thread::spawn(move || self.run_loop())
|
||||
}
|
||||
|
||||
fn run_loop(self) {
|
||||
let (mut state, tasks_receiver) = self.split();
|
||||
state.populate_router();
|
||||
for item in tasks_receiver.iter() {
|
||||
state.handle_item(item);
|
||||
}
|
||||
}
|
||||
|
||||
fn split(self) -> (ExecutorState, Receiver<QueueItem>) {
|
||||
(
|
||||
ExecutorState {
|
||||
result_handler: ExecutorState::build_router(self.config_data.items().len()),
|
||||
config_data: self.config_data,
|
||||
},
|
||||
self.tasks_receiver
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct ExecutorState {
|
||||
config_data: BaseConfig,
|
||||
result_handler: Sender<RouterCommand>,
|
||||
}
|
||||
|
||||
impl ExecutorState {
|
||||
fn handle_item(&mut self, item: QueueItem) {
|
||||
match item.action {
|
||||
QueueAction::GetAbout { respond_to } => {
|
||||
respond_to.send(self.config_data.get_about().clone()).unwrap_or(());
|
||||
},
|
||||
QueueAction::DoUpdate { index, value } => {
|
||||
if let Some(item) = self.config_data.get_item(index) {
|
||||
match super::Actor::build(item, (index, value)) {
|
||||
Ok(act) => {
|
||||
let respond_to = self.result_handler.clone();
|
||||
thread::spawn(move || {
|
||||
let result = act.run();
|
||||
match respond_to.send(RouterCommand::HandleResult{index, result}) {
|
||||
Ok(_) => {},
|
||||
Err(_) => log::warn!("Failed to send DoUpdate response for item #{}", index),
|
||||
}
|
||||
});
|
||||
},
|
||||
Err(e) => log::error!("Failed to build DoUpdate actor for item #{}: {}", index, e)
|
||||
}
|
||||
} else {
|
||||
log::warn!("Received DoUpdate on non-existent item #{} with value `{}`", index, super::primitive_utils::debug(&value))
|
||||
}
|
||||
},
|
||||
QueueAction::DoReload { respond_to } => {
|
||||
self.config_data = BaseConfig::load(crate::consts::FILEPATH);
|
||||
self.populate_router();
|
||||
respond_to.send(self.config_data.items().clone()).unwrap_or(());
|
||||
},
|
||||
QueueAction::SetCallback { index, respond_to } => {
|
||||
if let Some(elem) = self.config_data.get_item(index) {
|
||||
let display_of = match elem {
|
||||
ElementConfig::ResultDisplay(c) => c.result_of,
|
||||
_ => index,
|
||||
};
|
||||
if let Err(_) = self.result_handler.send(
|
||||
RouterCommand::AddSender {
|
||||
index: display_of,
|
||||
sender: respond_to,
|
||||
}) {
|
||||
log::warn!("Failed to send to ResultRouter, rebuilding router");
|
||||
self.result_handler = ExecutorState::build_router(self.config_data.items().len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_router(items_len: usize) -> Sender<RouterCommand> {
|
||||
let router = ResultRouter::build(&(), items_len).unwrap();
|
||||
let result = router.run();
|
||||
result
|
||||
}
|
||||
|
||||
fn populate_router(&mut self) {
|
||||
if let Err(_) = self.result_handler.send(RouterCommand::Clear{}) {
|
||||
return;
|
||||
}
|
||||
// start reading displays with periodic actions
|
||||
for (index, item) in self.config_data.items().iter().enumerate() {
|
||||
match item {
|
||||
ElementConfig::ReadingDisplay(r) => {
|
||||
if let Ok(actor) = super::PeriodicActor::build(r, (index, self.result_handler.clone())) {
|
||||
actor.run();
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
backend/src/runtime/mod.rs
Normal file
14
backend/src/runtime/mod.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
mod actor;
|
||||
mod command_actor;
|
||||
mod communication;
|
||||
mod executor;
|
||||
mod periodic_actor;
|
||||
mod primitive_utils;
|
||||
mod result_router;
|
||||
|
||||
pub use actor::{Actor, Act, ActError, ActorType};
|
||||
pub use command_actor::CommandActor;
|
||||
pub use communication::{QueueItem, QueueAction};
|
||||
pub use executor::RuntimeExecutor;
|
||||
pub use periodic_actor::PeriodicActor;
|
||||
pub use result_router::{ResultRouter, RouterCommand};
|
57
backend/src/runtime/periodic_actor.rs
Normal file
57
backend/src/runtime/periodic_actor.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use std::sync::mpsc::Sender;
|
||||
use std::time::Duration;
|
||||
|
||||
use usdpl_back::core::serdes::Primitive;
|
||||
|
||||
use crate::config::ReadingConfig;
|
||||
use super::{Act, ActError, ActorType, RouterCommand};
|
||||
|
||||
pub struct PeriodicActor {
|
||||
config: ReadingConfig,
|
||||
result_handler: Sender<RouterCommand>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl Act for PeriodicActor {
|
||||
type Param = (usize, Sender<RouterCommand>);
|
||||
type Config = ReadingConfig;
|
||||
type Return = ();
|
||||
|
||||
fn build(config: &Self::Config, parameter: Self::Param) -> Result<Self, ActError> {
|
||||
ActorType::build(&config.on_period, Primitive::Empty)?;
|
||||
Ok(
|
||||
Self {
|
||||
config: config.clone(),
|
||||
result_handler: parameter.1,
|
||||
index: parameter.0,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn run(self) -> Self::Return {
|
||||
std::thread::spawn(move || {
|
||||
let sleep_duration = Duration::from_millis(self.config.period_ms);
|
||||
loop {
|
||||
let actor = match ActorType::build(&self.config.on_period, Primitive::Empty) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
log::error!("PeriodicActor failed to build for item #{}: {}", self.index, e);
|
||||
break;
|
||||
}
|
||||
};
|
||||
let result = actor.run();
|
||||
match self.result_handler.send(RouterCommand::HandleResult {
|
||||
index: self.index, result
|
||||
}) {
|
||||
Ok(_) => {},
|
||||
Err(_e) => {
|
||||
log::warn!("PeriodicActor failed to handle result for item #{}", self.index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::thread::sleep(sleep_duration);
|
||||
}
|
||||
log::info!("PeriodicActor completed for #{}", self.index);
|
||||
});
|
||||
}
|
||||
}
|
84
backend/src/runtime/primitive_utils.rs
Normal file
84
backend/src/runtime/primitive_utils.rs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use usdpl_back::core::serdes::Primitive;
|
||||
|
||||
//use super::ActError;
|
||||
|
||||
/*macro_rules! map_primitive_number_impl {
|
||||
($type:ty, $type_name:literal, $fn_name:ident) => {
|
||||
pub fn $fn_name (param: Primitive) -> Result<$type, ActError> {
|
||||
match param {
|
||||
Primitive::I64(a) => Ok(a as $type),
|
||||
Primitive::I32(a) => Ok(a as $type),
|
||||
Primitive::U64(a) => Ok(a as $type),
|
||||
Primitive::U32(a) => Ok(a as $type),
|
||||
Primitive::F64(a) => Ok(a as $type),
|
||||
Primitive::F32(a) => Ok(a as $type),
|
||||
_ => Err(format!("Parameter must be {} type", $type_name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
/*macro_rules! map_primitive_impl {
|
||||
($type:ty, $primitive:ident, $type_name:literal, $fn_name:ident) => {
|
||||
pub fn $fn_name (param: Primitive) -> Result<$type, ActError> {
|
||||
match param {
|
||||
Primitive::$primitive(a) => Ok(a),
|
||||
_ => Err(format!("Parameter must be {} type", $type_name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
//map_primitive_impl!{bool, Bool, "boolean", try_primitive_bool}
|
||||
|
||||
//map_primitive_impl!{String, String, "string", try_primitive_string}
|
||||
|
||||
//map_primitive_number_impl!{usize, "uinteger", try_primitive_usize}
|
||||
|
||||
#[inline]
|
||||
pub fn debug(primitive: &Primitive) -> String {
|
||||
match primitive {
|
||||
Primitive::Empty => "Primitive::Empty".to_owned(),
|
||||
Primitive::String(x) => format!("Primitive::String(`{}`)", x),
|
||||
Primitive::F32(x) => format!("Primitive::F32(`{}`)", x),
|
||||
Primitive::F64(x) => format!("Primitive::F64(`{}`)", x),
|
||||
Primitive::U32(x) => format!("Primitive::U32(`{}`)", x),
|
||||
Primitive::U64(x) => format!("Primitive::U64(`{}`)", x),
|
||||
Primitive::I32(x) => format!("Primitive::I32(`{}`)", x),
|
||||
Primitive::I64(x) => format!("Primitive::I64(`{}`)", x),
|
||||
Primitive::Bool(x) => format!("Primitive::Bool(`{}`)", x),
|
||||
Primitive::Json(x) => format!("Primitive::Json(`{}`)", x),
|
||||
}
|
||||
}
|
||||
|
||||
/*#[inline]
|
||||
pub fn display(primitive: Primitive) -> String {
|
||||
match primitive {
|
||||
Primitive::Empty => "".to_owned(),
|
||||
Primitive::String(x) => x,
|
||||
Primitive::F32(x) => x.to_string(),
|
||||
Primitive::F64(x) => x.to_string(),
|
||||
Primitive::U32(x) => x.to_string(),
|
||||
Primitive::U64(x) => x.to_string(),
|
||||
Primitive::I32(x) => x.to_string(),
|
||||
Primitive::I64(x) => x.to_string(),
|
||||
Primitive::Bool(x) => x.to_string(),
|
||||
Primitive::Json(x) => x,
|
||||
}
|
||||
}*/
|
||||
|
||||
#[inline]
|
||||
pub fn clone(primitive: &Primitive) -> Primitive {
|
||||
match primitive {
|
||||
Primitive::Empty => Primitive::Empty,
|
||||
Primitive::String(x) => Primitive::String(x.clone()),
|
||||
Primitive::F32(x) => Primitive::F32(*x),
|
||||
Primitive::F64(x) => Primitive::F64(*x),
|
||||
Primitive::U32(x) => Primitive::U32(*x),
|
||||
Primitive::U64(x) => Primitive::U64(*x),
|
||||
Primitive::I32(x) => Primitive::I32(*x),
|
||||
Primitive::I64(x) => Primitive::I64(*x),
|
||||
Primitive::Bool(x) => Primitive::Bool(*x),
|
||||
Primitive::Json(x) => Primitive::Json(x.clone()),
|
||||
}
|
||||
}
|
133
backend/src/runtime/result_router.rs
Normal file
133
backend/src/runtime/result_router.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
use std::sync::mpsc::{self, Receiver, Sender};
|
||||
|
||||
use usdpl_back::core::serdes::Primitive;
|
||||
|
||||
//use crate::config::ElementConfig;
|
||||
use super::{Act, ActError};
|
||||
|
||||
const MAX_HANDLERS_PER_ITEM: usize = 8;
|
||||
|
||||
pub enum RouterCommand {
|
||||
AddSender {
|
||||
index: usize,
|
||||
sender: Sender<Primitive>,
|
||||
},
|
||||
HandleResult {
|
||||
index: usize,
|
||||
result: Primitive,
|
||||
},
|
||||
Clear{}
|
||||
}
|
||||
|
||||
pub struct ResultRouter {
|
||||
comm: Receiver<RouterCommand>,
|
||||
senders: Vec<[Option<Sender<Primitive>>; MAX_HANDLERS_PER_ITEM]>,
|
||||
comm_tx: Option<Sender<RouterCommand>>,
|
||||
cache: Vec<Option<Primitive>>,
|
||||
}
|
||||
|
||||
impl ResultRouter {
|
||||
fn all_senders_none(senders: &[Option<Sender<Primitive>>]) -> bool {
|
||||
let mut all_none = true;
|
||||
for s in senders.iter() {
|
||||
all_none &= s.is_none();
|
||||
}
|
||||
all_none
|
||||
}
|
||||
}
|
||||
|
||||
impl Act for ResultRouter {
|
||||
type Param = usize;
|
||||
type Config = ();
|
||||
type Return = Sender<RouterCommand>;
|
||||
|
||||
fn build(_config: &Self::Config, parameter: Self::Param) -> Result<Self, ActError> {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let mut cache_vec = Vec::with_capacity(parameter);
|
||||
for _ in 0..parameter {
|
||||
cache_vec.push(None);
|
||||
}
|
||||
Ok(Self {
|
||||
comm: rx,
|
||||
senders: vec![[(); MAX_HANDLERS_PER_ITEM].map(|_| None); parameter],
|
||||
comm_tx: Some(tx),
|
||||
cache: cache_vec,
|
||||
})
|
||||
}
|
||||
|
||||
fn run(mut self) -> Self::Return {
|
||||
let result = self.comm_tx.take().unwrap();
|
||||
std::thread::spawn(move || {
|
||||
log::debug!("ResultRouter starting");
|
||||
for command in self.comm.iter() {
|
||||
match command {
|
||||
RouterCommand::AddSender { index, sender } => {
|
||||
log::debug!("Handling AddSender for item #{}", index);
|
||||
if let Some(senders) = self.senders.get_mut(index) {
|
||||
// send cached value, if available
|
||||
if self.cache[index].is_some() {
|
||||
log::debug!("Routing cached result for item #{}", index);
|
||||
let result = self.cache[index].take().unwrap();
|
||||
match sender.send(result) {
|
||||
Ok(_) => {},
|
||||
Err(e) => {
|
||||
self.cache[index] = Some(e.0);
|
||||
log::debug!("ResultRouter ignoring AddSender since sending cached value failed");
|
||||
continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
// save sender for future results
|
||||
let mut was_set = false;
|
||||
'inner_loop: for sender_opt in senders {
|
||||
if sender_opt.is_none() {
|
||||
*sender_opt = Some(sender);
|
||||
was_set = true;
|
||||
break 'inner_loop;
|
||||
}
|
||||
}
|
||||
if !was_set {
|
||||
log::warn!("ResultRouter could not add another sender for index {}", index);
|
||||
}
|
||||
} else {
|
||||
log::warn!("ResultRouter got AddSender command for invalid index {} (max: {})", index, self.senders.len());
|
||||
}
|
||||
}
|
||||
RouterCommand::HandleResult {index, result} => {
|
||||
log::debug!("Handling HandleResult for item #{}", index);
|
||||
if let Some(senders) = self.senders.get_mut(index) {
|
||||
if Self::all_senders_none(senders) {
|
||||
self.cache[index] = Some(result);
|
||||
log::debug!("Cached result for item #{}", index);
|
||||
} else {
|
||||
for (i, sender_opt) in senders.iter_mut().enumerate() {
|
||||
if let Some(sender) = sender_opt {
|
||||
match sender.send(super::primitive_utils::clone(&result)) {
|
||||
Ok(_) => {},
|
||||
Err(_) => {
|
||||
log::debug!("Removing sender {} because it seems closed", i);
|
||||
*sender_opt = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("Routed result for item #{}", index);
|
||||
}
|
||||
} else {
|
||||
log::warn!("ResultRouter got AddSender command for invalid index {} (max: {})", index, self.senders.len());
|
||||
}
|
||||
},
|
||||
RouterCommand::Clear {} => {
|
||||
log::debug!("Handling Clear");
|
||||
for i in 0..self.senders.len() {
|
||||
self.senders[i] = [(); MAX_HANDLERS_PER_ITEM].map(|_| None);
|
||||
self.cache[i] = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log::warn!("ResultRouter completed");
|
||||
});
|
||||
result
|
||||
}
|
||||
}
|
57
kaylon.json
Normal file
57
kaylon.json
Normal file
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"api-version": "v0.0.0",
|
||||
"items": [
|
||||
{
|
||||
"element": "button",
|
||||
"title": "Test Button",
|
||||
"on_click": {
|
||||
"action": "command",
|
||||
"run": "echo 'hello button'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"element": "toggle",
|
||||
"title": "Test Toggle",
|
||||
"description": "Toggle description",
|
||||
"on_toggle": {
|
||||
"action": "command",
|
||||
"run": "echo 'hello toggle ${KAYLON_VALUE}'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"element": "slider",
|
||||
"title": "Test Slider",
|
||||
"min": 0,
|
||||
"max": 3,
|
||||
"notches": null,
|
||||
"on_set": {
|
||||
"action": "command",
|
||||
"run": "echo 'hello slider'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"element": "reading",
|
||||
"title": "Test Reading",
|
||||
"period_ms": 10000,
|
||||
"on_period": {
|
||||
"action": "command",
|
||||
"run": "echo 'hello reading'"
|
||||
}
|
||||
},
|
||||
{
|
||||
"element": "result-display",
|
||||
"title": "Test Result",
|
||||
"result_of": 1
|
||||
}
|
||||
],
|
||||
"about": {
|
||||
"name": "Test name",
|
||||
"version": "v0.42.0",
|
||||
"description": "Test description",
|
||||
"url": "https://github.com/NGnius/kaylon",
|
||||
"authors": [
|
||||
"NGnius <ngniusness@gmail.com>"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
4
main.py
4
main.py
|
@ -11,6 +11,6 @@ class Plugin:
|
|||
# Asyncio-compatible long-running code, executed in a task when the plugin is loaded
|
||||
async def _main(self):
|
||||
# startup
|
||||
self.backend_proc = subprocess.Popen([PARENT_DIR + "/bin/backend"])
|
||||
#self.backend_proc = subprocess.Popen([PARENT_DIR + "/bin/backend"])
|
||||
while True:
|
||||
asyncio.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
"typescript": "^4.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"decky-frontend-lib": "^1.0.1",
|
||||
"decky-frontend-lib": "*",
|
||||
"react-icons": "^4.3.1",
|
||||
"usdpl-front": "file:./src/usdpl_front"
|
||||
},
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
cargo build --release
|
||||
mkdir ../bin
|
||||
# TODO replace "backend" \/ with binary name
|
||||
cp ./target/release/backend ../bin/backend
|
|
@ -1,11 +0,0 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct AboutConfig {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub description: String,
|
||||
pub url: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub license: Option<String>,
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use super::{ElementConfig, AboutConfig};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(tag = "api-version")]
|
||||
pub enum BaseConfig {
|
||||
#[serde(rename = "v0.0.0")]
|
||||
V0 {
|
||||
items: Vec<ElementConfig>,
|
||||
about: AboutConfig,
|
||||
},
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
mod config;
|
||||
|
||||
use simplelog::{WriteLogger, LevelFilter};
|
||||
|
||||
use usdpl_back::Instance;
|
||||
use usdpl_back::core::serdes::Primitive;
|
||||
|
||||
const PORT: u16 = 54321; // TODO replace with something unique
|
||||
|
||||
const PACKAGE_NAME: &'static str = env!("CARGO_PKG_NAME");
|
||||
const PACKAGE_VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn main() -> Result<(), ()> {
|
||||
let log_filepath = format!("/tmp/{}.log", PACKAGE_NAME);
|
||||
WriteLogger::init(
|
||||
#[cfg(debug_assertions)]{LevelFilter::Debug},
|
||||
#[cfg(not(debug_assertions))]{LevelFilter::Info},
|
||||
Default::default(),
|
||||
std::fs::File::create(&log_filepath).unwrap()
|
||||
).unwrap();
|
||||
|
||||
log::info!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION);
|
||||
println!("Starting back-end ({} v{})", PACKAGE_NAME, PACKAGE_VERSION);
|
||||
Instance::new(PORT)
|
||||
.register("hello", |_: Vec<Primitive>| vec![format!("Hello {}", PACKAGE_NAME).into()])
|
||||
.run_blocking()
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import {init_usdpl, target, init_embedded, call_backend} from "usdpl-front";
|
||||
import {init_usdpl, target_usdpl, init_embedded, call_backend} from "usdpl-front";
|
||||
|
||||
const USDPL_PORT: number = 25717;
|
||||
|
||||
|
@ -20,7 +20,7 @@ export async function initBackend() {
|
|||
// init usdpl
|
||||
await init_embedded();
|
||||
init_usdpl(USDPL_PORT);
|
||||
console.log("USDPL started for framework: " + target());
|
||||
console.log("USDPL started for framework: " + target_usdpl());
|
||||
//setReady(true);
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ export type CAbout = {
|
|||
version: string;
|
||||
description: string;
|
||||
url: string | null;
|
||||
author: string | null;
|
||||
authors: string[];
|
||||
license: string | null;
|
||||
}
|
||||
|
||||
|
@ -58,18 +58,24 @@ export type CReading = {
|
|||
period_ms: number;
|
||||
}
|
||||
|
||||
export type CElement = CButton | CToggle | CSlider | CReading;
|
||||
export type CResultDisplay = {
|
||||
element: string; // "result-display"
|
||||
title: string;
|
||||
result_of: number;
|
||||
}
|
||||
|
||||
export type CElement = CButton | CToggle | CSlider | CReading | CResultDisplay;
|
||||
|
||||
export async function getElements(): Promise<CElement[]> {
|
||||
return await call_backend("get_items", []);
|
||||
return (await call_backend("get_items", []))[0];
|
||||
}
|
||||
|
||||
export async function onUpdate(index: number, value: any): Promise<any> {
|
||||
return (await call_backend("on_update", [index, value]))[0];
|
||||
}
|
||||
|
||||
export async function getReading(index: number): Promise<string | null> {
|
||||
return (await call_backend("get_reading", [index]))[0];
|
||||
export async function getDisplay(index: number): Promise<string | null> {
|
||||
return (await call_backend("get_display", [index]))[0];
|
||||
}
|
||||
|
||||
export async function getAbout(): Promise<CAbout> {
|
||||
|
@ -77,5 +83,5 @@ export async function getAbout(): Promise<CAbout> {
|
|||
}
|
||||
|
||||
export async function reload(): Promise<CElement[]> {
|
||||
return await call_backend("reload", []);
|
||||
return (await call_backend("reload", []))[0];
|
||||
}
|
||||
|
|
257
src/index.tsx
257
src/index.tsx
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
ButtonItem,
|
||||
definePlugin,
|
||||
DialogButton,
|
||||
//DialogButton,
|
||||
//Menu,
|
||||
//MenuItem,
|
||||
PanelSection,
|
||||
|
@ -19,72 +19,93 @@ import {
|
|||
import { VFC, useState } from "react";
|
||||
import { GiWashingMachine } from "react-icons/gi";
|
||||
|
||||
import { call_backend } from "usdpl-front";
|
||||
import { get_value, set_value } from "usdpl-front";
|
||||
import * as backend from "./backend";
|
||||
|
||||
// interface AddMethodArgs {
|
||||
// left: number;
|
||||
// right: number;
|
||||
// }
|
||||
|
||||
const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard);
|
||||
|
||||
const DISPLAY_KEY = "display";
|
||||
const VALUE_KEY = "value";
|
||||
|
||||
let items: backend.CElement[] = [];
|
||||
let about: backend.CAbout | null = null;
|
||||
|
||||
let update = () => {};
|
||||
|
||||
function displayCallback(index: number) {
|
||||
return (newVal: any) => {
|
||||
set_value(DISPLAY_KEY + index.toString(), newVal);
|
||||
backend.resolve(backend.getDisplay(index), displayCallback(index));
|
||||
console.log("Got display for " + index.toString(), newVal);
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
// init USDPL WASM frontend
|
||||
// this is required to interface with the backend
|
||||
(async () => {
|
||||
await backend.initBackend();
|
||||
let about_promise = backend.getAbout();
|
||||
let elements_promise = backend.getElements();
|
||||
about = await about_promise;
|
||||
console.log("KAYLON: got about", about);
|
||||
let result = await elements_promise;
|
||||
console.log("KAYLON: got elements", result);
|
||||
if (result != null) {
|
||||
items = await backend.getElements();
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
console.log("KAYLON: req display for item #" + i.toString());
|
||||
backend.resolve(backend.getDisplay(i), displayCallback(i));
|
||||
}
|
||||
} else {
|
||||
console.warn("KAYLON: backend connection failed");
|
||||
}
|
||||
})();
|
||||
|
||||
const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => {
|
||||
// const [result, setResult] = useState<number | undefined>();
|
||||
|
||||
// const onClick = async () => {
|
||||
// const result = await serverAPI.callPluginMethod<AddMethodArgs, number>(
|
||||
// "add",
|
||||
// {
|
||||
// left: 2,
|
||||
// right: 2,
|
||||
// }
|
||||
// );
|
||||
// if (result.success) {
|
||||
// setResult(result.result);
|
||||
// }
|
||||
// };
|
||||
|
||||
const [triggerInternal, updateInternal] = useState<boolean>(false);
|
||||
|
||||
function update() {
|
||||
update = () => {
|
||||
updateInternal(!triggerInternal);
|
||||
}
|
||||
|
||||
function updateIdc(_: any) {
|
||||
update();
|
||||
}
|
||||
|
||||
// call hello callback on backend
|
||||
(async () => {
|
||||
let response = await call_backend("hello", []);
|
||||
console.log("Backend says:", response);
|
||||
})();
|
||||
|
||||
return (
|
||||
<PanelSection title="Panel Section">
|
||||
<PanelSection>
|
||||
{items.map(
|
||||
(elem, i) => {
|
||||
return <PanelSectionRow>{buildHtmlElement(elem, i, updateIdc)}</PanelSectionRow>
|
||||
})
|
||||
}
|
||||
{ about != null && buildAbout() }
|
||||
<PanelSectionRow>
|
||||
<ButtonItem
|
||||
layout="below"
|
||||
onClick={(_: MouseEvent) => {
|
||||
backend.resolve(backend.reload(),
|
||||
(reload_items: backend.CElement[]) => {
|
||||
items = reload_items;
|
||||
console.log("KAYLON: got elements", reload_items);
|
||||
update();
|
||||
});
|
||||
backend.resolve(backend.getAbout(),
|
||||
(new_about: backend.CAbout) => {
|
||||
about = new_about;
|
||||
console.log("KAYLON: got about", about);
|
||||
update();
|
||||
});
|
||||
}}>
|
||||
Reload
|
||||
</ButtonItem>
|
||||
</PanelSectionRow>
|
||||
</PanelSection>
|
||||
);
|
||||
};
|
||||
|
||||
const DeckyPluginRouterTest: VFC = () => {
|
||||
return (
|
||||
<div style={{ marginTop: "50px", color: "white" }}>
|
||||
Hello World!
|
||||
<DialogButton onClick={() => {}}>
|
||||
Go to Store
|
||||
</DialogButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function buildHtmlElement(element: backend.CElement, index: number, updateIdc: any) {
|
||||
switch (element.element) {
|
||||
case "button":
|
||||
|
@ -95,8 +116,11 @@ function buildHtmlElement(element: backend.CElement, index: number, updateIdc: a
|
|||
return buildToggle(element as backend.CToggle, index, updateIdc);
|
||||
case "reading":
|
||||
return buildReading(element as backend.CReading, index, updateIdc);
|
||||
case "result-display":
|
||||
return buildResultDisplay(element as backend.CResultDisplay, index, updateIdc);
|
||||
}
|
||||
return "Unsupported";
|
||||
console.error("KAYLON: Unsupported element", element);
|
||||
return <div>Unsupported</div>;
|
||||
}
|
||||
|
||||
function buildButton(element: backend.CButton, index: number, updateIdc: any) {
|
||||
|
@ -110,62 +134,175 @@ function buildButton(element: backend.CButton, index: number, updateIdc: any) {
|
|||
}
|
||||
|
||||
function buildSlider(element: backend.CSlider, index: number, updateIdc: any) {
|
||||
const KEY = VALUE_KEY + index.toString();
|
||||
if (get_value(KEY) == null) {
|
||||
set_value(KEY, element.min);
|
||||
}
|
||||
return (
|
||||
<SliderField
|
||||
label={element.title}
|
||||
value={element.min}
|
||||
value={get_value(KEY)}
|
||||
max={element.max}
|
||||
min={element.min}
|
||||
showValue={true}
|
||||
onChange={(value: number) => {
|
||||
backend.resolve(backend.onUpdate(index, value), updateIdc)
|
||||
backend.resolve(backend.onUpdate(index, value), updateIdc);
|
||||
set_value(KEY, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function buildToggle(element: backend.CToggle, index: number, updateIdc: any) {
|
||||
const KEY = VALUE_KEY + index.toString();
|
||||
if (get_value(KEY) == null) {
|
||||
set_value(KEY, false);
|
||||
}
|
||||
return (
|
||||
<ToggleField
|
||||
checked={false}
|
||||
checked={get_value(KEY)}
|
||||
label={element.title}
|
||||
description={element.description!}
|
||||
onChange={(value: boolean) => {
|
||||
backend.resolve(backend.onUpdate(index, value), updateIdc)
|
||||
backend.resolve(backend.onUpdate(index, value), updateIdc);
|
||||
set_value(KEY, value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function buildReading(element: backend.CReading, _index: number, _updateIdc: any) {
|
||||
function buildReading(element: backend.CReading, index: number, _updateIdc: any) {
|
||||
return (
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>{element.title}</div>
|
||||
<div className={gamepadDialogClasses.FieldChildren}>{"idk"}</div>
|
||||
<div className={gamepadDialogClasses.FieldChildren}>{get_value(DISPLAY_KEY + index.toString())}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default definePlugin((serverApi: ServerAPI) => {
|
||||
serverApi.routerHook.addRoute("/decky-plugin-test", DeckyPluginRouterTest, {
|
||||
exact: true,
|
||||
});
|
||||
|
||||
// init USDPL WASM frontend
|
||||
// this is required to interface with the backend
|
||||
(async () => {
|
||||
await backend.initBackend();
|
||||
items = await backend.getElements();
|
||||
})();
|
||||
function buildResultDisplay(element: backend.CResultDisplay, index: number, _updateIdc: any) {
|
||||
return (
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>{element.title}</div>
|
||||
<div className={gamepadDialogClasses.FieldChildren}>{get_value(DISPLAY_KEY + index.toString())}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildAbout() {
|
||||
if (about == null) {
|
||||
return [];
|
||||
} else {
|
||||
let elements = [
|
||||
<div className={staticClasses.PanelSectionTitle}>
|
||||
About
|
||||
</div>,
|
||||
<PanelSectionRow>
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>Name</div>
|
||||
<div className={gamepadDialogClasses.FieldChildren}>{about.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSectionRow>,
|
||||
<PanelSectionRow>
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>Version</div>
|
||||
<div className={gamepadDialogClasses.FieldChildren}>{about.version}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSectionRow>,
|
||||
<PanelSectionRow>
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>Description</div>
|
||||
<div className={gamepadDialogClasses.FieldDescription}>{about.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
];
|
||||
if (about.url != null) {
|
||||
elements.push(
|
||||
<PanelSectionRow>
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>URL</div>
|
||||
<div className={gamepadDialogClasses.FieldDescription}>{about.url}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
);
|
||||
}
|
||||
if (about.authors.length > 1) {
|
||||
let authors = about.authors.map((elem, i) => {
|
||||
if (i == about!.authors.length - 1) {
|
||||
return <p>{elem}</p>;
|
||||
} else {
|
||||
return <span>{elem}</span>;
|
||||
}
|
||||
});
|
||||
elements.push(
|
||||
<PanelSectionRow>
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>Authors</div>
|
||||
<div className={gamepadDialogClasses.FieldDescription}>{authors}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
);
|
||||
} else if (about.authors.length == 1) {
|
||||
elements.push(
|
||||
<PanelSectionRow>
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>Author</div>
|
||||
<div className={gamepadDialogClasses.FieldDescription}>{about.authors[0]}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
);
|
||||
} else {
|
||||
elements.push(
|
||||
<PanelSectionRow>
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>Author</div>
|
||||
<div className={gamepadDialogClasses.FieldDescription}>NGnius</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
);
|
||||
}
|
||||
|
||||
if (about.license != null) {
|
||||
elements.push(
|
||||
<PanelSectionRow>
|
||||
<div className={FieldWithSeparator}>
|
||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||
<div className={gamepadDialogClasses.FieldLabel}>License</div>
|
||||
<div className={gamepadDialogClasses.FieldChildren}>{about.license}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PanelSectionRow>
|
||||
);
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
}
|
||||
|
||||
export default definePlugin((serverApi: ServerAPI) => {
|
||||
return {
|
||||
title: <div className={staticClasses.Title}>Example Plugin</div>,
|
||||
title: <div className={staticClasses.Title}>{about == null? "Kaylon": about.name}</div>,
|
||||
content: <Content serverAPI={serverApi} />,
|
||||
icon: <GiWashingMachine />,
|
||||
onDismount() {
|
||||
serverApi.routerHook.removeRoute("/decky-plugin-test");
|
||||
//serverApi.routerHook.removeRoute("/decky-plugin-test");
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
9
src/usdpl_front/README.md
Normal file
9
src/usdpl_front/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
[![Crates.io](https://img.shields.io/crates/v/usdpl-front?style=flat-square)](https://crates.io/crates/usdpl-front)
|
||||
|
||||
# usdpl-front-front
|
||||
|
||||
Front-end library to be called from Javascript.
|
||||
Targets WASM.
|
||||
|
||||
In true Javascript tradition, this part of the library does not support error handling.
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
"NGnius (Graham) <ngniusness@gmail.com>"
|
||||
],
|
||||
"description": "Universal Steam Deck Plugin Library front-end designed for WASM",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.2",
|
||||
"license": "GPL-3.0-only",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
25
src/usdpl_front/usdpl_front.d.ts
vendored
25
src/usdpl_front/usdpl_front.d.ts
vendored
|
@ -9,7 +9,25 @@ export function init_usdpl(port: number): void;
|
|||
* Get the targeted plugin framework, or "any" if unknown
|
||||
* @returns {string}
|
||||
*/
|
||||
export function target(): string;
|
||||
export function target_usdpl(): string;
|
||||
/**
|
||||
* Get the UDSPL front-end version
|
||||
* @returns {string}
|
||||
*/
|
||||
export function version_usdpl(): string;
|
||||
/**
|
||||
* Get the targeted plugin framework, or "any" if unknown
|
||||
* @param {string} key
|
||||
* @param {any} value
|
||||
* @returns {any}
|
||||
*/
|
||||
export function set_value(key: string, value: any): any;
|
||||
/**
|
||||
* Get the targeted plugin framework, or "any" if unknown
|
||||
* @param {string} key
|
||||
* @returns {any}
|
||||
*/
|
||||
export function get_value(key: string): any;
|
||||
/**
|
||||
* Call a function on the back-end.
|
||||
* Returns null (None) if this fails for any reason.
|
||||
|
@ -24,7 +42,10 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
|
|||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly init_usdpl: (a: number) => void;
|
||||
readonly target: (a: number) => void;
|
||||
readonly target_usdpl: (a: number) => void;
|
||||
readonly version_usdpl: (a: number) => void;
|
||||
readonly set_value: (a: number, b: number, c: number) => number;
|
||||
readonly get_value: (a: number, b: number) => number;
|
||||
readonly call_backend: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_export_0: (a: number) => number;
|
||||
readonly __wbindgen_export_1: (a: number, b: number, c: number) => number;
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
5
src/usdpl_front/usdpl_front_bg.wasm.d.ts
vendored
5
src/usdpl_front/usdpl_front_bg.wasm.d.ts
vendored
|
@ -2,7 +2,10 @@
|
|||
/* eslint-disable */
|
||||
export const memory: WebAssembly.Memory;
|
||||
export function init_usdpl(a: number): void;
|
||||
export function target(a: number): void;
|
||||
export function target_usdpl(a: number): void;
|
||||
export function version_usdpl(a: number): void;
|
||||
export function set_value(a: number, b: number, c: number): number;
|
||||
export function get_value(a: number, b: number): number;
|
||||
export function call_backend(a: number, b: number, c: number, d: number): number;
|
||||
export function __wbindgen_export_0(a: number): number;
|
||||
export function __wbindgen_export_1(a: number, b: number, c: number): number;
|
||||
|
|
Reference in a new issue