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
|
.vscode/settings.json
|
||||||
|
|
||||||
# ignore Rust compiler files
|
# ignore Rust compiler files
|
||||||
/server/target
|
/backend/target
|
||||||
backend
|
/backend/out
|
||||||
/bin
|
/bin
|
||||||
|
|
||||||
# packaged teasers
|
# 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.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
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]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
@ -349,10 +371,12 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
||||||
name = "kaylon"
|
name = "kaylon"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"simplelog",
|
"simplelog",
|
||||||
|
"tokio",
|
||||||
"usdpl-back",
|
"usdpl-back",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -959,11 +983,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usdpl-back"
|
name = "usdpl-back"
|
||||||
version = "0.6.0"
|
version = "0.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cbbc0781e83ba990f8239142e33173a2d2548701775f3db66702d1af4fd0319a"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"async-recursion",
|
||||||
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"log",
|
||||||
"tokio",
|
"tokio",
|
||||||
"usdpl-core",
|
"usdpl-core",
|
||||||
"warp",
|
"warp",
|
||||||
|
@ -972,8 +997,6 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usdpl-core"
|
name = "usdpl-core"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "862153581fac266458521f49e5906a71c1eee1665cb4c7d71e9586bd34b45394"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
]
|
]
|
|
@ -6,8 +6,13 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[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 = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
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 serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use super::{ButtonConfig, ToggleConfig, SliderConfig, ReadingConfig};
|
use super::{ButtonConfig, ToggleConfig, SliderConfig, ReadingConfig, ResultDisplayConfig};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
#[serde(tag = "element")]
|
#[serde(tag = "element")]
|
||||||
|
@ -12,5 +12,7 @@ pub enum ElementConfig {
|
||||||
#[serde(rename = "slider")]
|
#[serde(rename = "slider")]
|
||||||
Slider(SliderConfig),
|
Slider(SliderConfig),
|
||||||
#[serde(rename = "reading")]
|
#[serde(rename = "reading")]
|
||||||
Reading(ReadingConfig),
|
ReadingDisplay(ReadingConfig),
|
||||||
|
#[serde(rename = "result-display")]
|
||||||
|
ResultDisplay(ResultDisplayConfig),
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ mod base;
|
||||||
mod button;
|
mod button;
|
||||||
mod element;
|
mod element;
|
||||||
mod reading;
|
mod reading;
|
||||||
|
mod result_display;
|
||||||
mod slider;
|
mod slider;
|
||||||
mod toggle;
|
mod toggle;
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ pub use base::BaseConfig;
|
||||||
pub use button::ButtonConfig;
|
pub use button::ButtonConfig;
|
||||||
pub use element::ElementConfig;
|
pub use element::ElementConfig;
|
||||||
pub use reading::ReadingConfig;
|
pub use reading::ReadingConfig;
|
||||||
|
pub use result_display::ResultDisplayConfig;
|
||||||
pub use slider::SliderConfig;
|
pub use slider::SliderConfig;
|
||||||
pub use toggle::ToggleConfig;
|
pub use toggle::ToggleConfig;
|
||||||
|
|
||||||
|
@ -31,8 +33,7 @@ mod test {
|
||||||
ElementConfig::Toggle(ToggleConfig {
|
ElementConfig::Toggle(ToggleConfig {
|
||||||
title: "Test Toggle".into(),
|
title: "Test Toggle".into(),
|
||||||
description: Some("Toggle description".into()),
|
description: Some("Toggle description".into()),
|
||||||
on_enable: ActionConfig::Command(CommandAction{run: "echo 'hello toggle 1'".into()}),
|
on_toggle: ActionConfig::Command(CommandAction{run: "echo 'hello toggle $KAYLON_VALUE'".into()}),
|
||||||
on_disable: ActionConfig::Command(CommandAction{run: "echo 'hello toggle 0'".into()}),
|
|
||||||
}),
|
}),
|
||||||
ElementConfig::Slider(SliderConfig {
|
ElementConfig::Slider(SliderConfig {
|
||||||
title: "Test Slider".into(),
|
title: "Test Slider".into(),
|
||||||
|
@ -41,18 +42,22 @@ mod test {
|
||||||
notches: None,
|
notches: None,
|
||||||
on_set: ActionConfig::Command(CommandAction{run: "echo 'hello slider'".into()}),
|
on_set: ActionConfig::Command(CommandAction{run: "echo 'hello slider'".into()}),
|
||||||
}),
|
}),
|
||||||
ElementConfig::Reading(ReadingConfig {
|
ElementConfig::ReadingDisplay(ReadingConfig {
|
||||||
title: "Test Reading".into(),
|
title: "Test Reading".into(),
|
||||||
period_ms: 10000,
|
period_ms: 10000,
|
||||||
on_period: ActionConfig::Command(CommandAction{run: "echo 'hello reading'".into()})
|
on_period: ActionConfig::Command(CommandAction{run: "echo 'hello reading'".into()})
|
||||||
}),
|
}),
|
||||||
|
ElementConfig::ResultDisplay(ResultDisplayConfig {
|
||||||
|
title: "Test Reading".into(),
|
||||||
|
result_of: 1,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
about: AboutConfig {
|
about: AboutConfig {
|
||||||
name: "Test name".into(),
|
name: "Test name".into(),
|
||||||
version: "v0.42.0".into(),
|
version: "v0.42.0".into(),
|
||||||
description: "Test description".into(),
|
description: "Test description".into(),
|
||||||
url: Some("https://github.com/NGnius/kaylon".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()),
|
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 struct ToggleConfig {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub on_enable: ActionConfig,
|
pub on_toggle: ActionConfig,
|
||||||
pub on_disable: 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
|
# Asyncio-compatible long-running code, executed in a task when the plugin is loaded
|
||||||
async def _main(self):
|
async def _main(self):
|
||||||
# startup
|
# startup
|
||||||
self.backend_proc = subprocess.Popen([PARENT_DIR + "/bin/backend"])
|
#self.backend_proc = subprocess.Popen([PARENT_DIR + "/bin/backend"])
|
||||||
while True:
|
while True:
|
||||||
asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
"typescript": "^4.6.4"
|
"typescript": "^4.6.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"decky-frontend-lib": "^1.0.1",
|
"decky-frontend-lib": "*",
|
||||||
"react-icons": "^4.3.1",
|
"react-icons": "^4.3.1",
|
||||||
"usdpl-front": "file:./src/usdpl_front"
|
"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;
|
const USDPL_PORT: number = 25717;
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ export async function initBackend() {
|
||||||
// init usdpl
|
// init usdpl
|
||||||
await init_embedded();
|
await init_embedded();
|
||||||
init_usdpl(USDPL_PORT);
|
init_usdpl(USDPL_PORT);
|
||||||
console.log("USDPL started for framework: " + target());
|
console.log("USDPL started for framework: " + target_usdpl());
|
||||||
//setReady(true);
|
//setReady(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ export type CAbout = {
|
||||||
version: string;
|
version: string;
|
||||||
description: string;
|
description: string;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
author: string | null;
|
authors: string[];
|
||||||
license: string | null;
|
license: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,18 +58,24 @@ export type CReading = {
|
||||||
period_ms: number;
|
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[]> {
|
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> {
|
export async function onUpdate(index: number, value: any): Promise<any> {
|
||||||
return (await call_backend("on_update", [index, value]))[0];
|
return (await call_backend("on_update", [index, value]))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReading(index: number): Promise<string | null> {
|
export async function getDisplay(index: number): Promise<string | null> {
|
||||||
return (await call_backend("get_reading", [index]))[0];
|
return (await call_backend("get_display", [index]))[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAbout(): Promise<CAbout> {
|
export async function getAbout(): Promise<CAbout> {
|
||||||
|
@ -77,5 +83,5 @@ export async function getAbout(): Promise<CAbout> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function reload(): Promise<CElement[]> {
|
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 {
|
import {
|
||||||
ButtonItem,
|
ButtonItem,
|
||||||
definePlugin,
|
definePlugin,
|
||||||
DialogButton,
|
//DialogButton,
|
||||||
//Menu,
|
//Menu,
|
||||||
//MenuItem,
|
//MenuItem,
|
||||||
PanelSection,
|
PanelSection,
|
||||||
|
@ -19,37 +19,54 @@ import {
|
||||||
import { VFC, useState } from "react";
|
import { VFC, useState } from "react";
|
||||||
import { GiWashingMachine } from "react-icons/gi";
|
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";
|
import * as backend from "./backend";
|
||||||
|
|
||||||
// interface AddMethodArgs {
|
|
||||||
// left: number;
|
|
||||||
// right: number;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard);
|
const FieldWithSeparator = joinClassNames(gamepadDialogClasses.Field, gamepadDialogClasses.WithBottomSeparatorStandard);
|
||||||
|
|
||||||
|
const DISPLAY_KEY = "display";
|
||||||
|
const VALUE_KEY = "value";
|
||||||
|
|
||||||
let items: backend.CElement[] = [];
|
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 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);
|
const [triggerInternal, updateInternal] = useState<boolean>(false);
|
||||||
|
|
||||||
function update() {
|
update = () => {
|
||||||
updateInternal(!triggerInternal);
|
updateInternal(!triggerInternal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,34 +74,38 @@ const Content: VFC<{ serverAPI: ServerAPI }> = ({}) => {
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
// call hello callback on backend
|
|
||||||
(async () => {
|
|
||||||
let response = await call_backend("hello", []);
|
|
||||||
console.log("Backend says:", response);
|
|
||||||
})();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelSection title="Panel Section">
|
<PanelSection>
|
||||||
{items.map(
|
{items.map(
|
||||||
(elem, i) => {
|
(elem, i) => {
|
||||||
return <PanelSectionRow>{buildHtmlElement(elem, i, updateIdc)}</PanelSectionRow>
|
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>
|
</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) {
|
function buildHtmlElement(element: backend.CElement, index: number, updateIdc: any) {
|
||||||
switch (element.element) {
|
switch (element.element) {
|
||||||
case "button":
|
case "button":
|
||||||
|
@ -95,8 +116,11 @@ function buildHtmlElement(element: backend.CElement, index: number, updateIdc: a
|
||||||
return buildToggle(element as backend.CToggle, index, updateIdc);
|
return buildToggle(element as backend.CToggle, index, updateIdc);
|
||||||
case "reading":
|
case "reading":
|
||||||
return buildReading(element as backend.CReading, index, updateIdc);
|
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) {
|
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) {
|
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 (
|
return (
|
||||||
<SliderField
|
<SliderField
|
||||||
label={element.title}
|
label={element.title}
|
||||||
value={element.min}
|
value={get_value(KEY)}
|
||||||
max={element.max}
|
max={element.max}
|
||||||
min={element.min}
|
min={element.min}
|
||||||
showValue={true}
|
showValue={true}
|
||||||
onChange={(value: number) => {
|
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) {
|
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 (
|
return (
|
||||||
<ToggleField
|
<ToggleField
|
||||||
checked={false}
|
checked={get_value(KEY)}
|
||||||
label={element.title}
|
label={element.title}
|
||||||
description={element.description!}
|
description={element.description!}
|
||||||
onChange={(value: boolean) => {
|
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 (
|
return (
|
||||||
<div className={FieldWithSeparator}>
|
<div className={FieldWithSeparator}>
|
||||||
<div className={gamepadDialogClasses.FieldLabelRow}>
|
<div className={gamepadDialogClasses.FieldLabelRow}>
|
||||||
<div className={gamepadDialogClasses.FieldLabel}>{element.title}</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
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();
|
|
||||||
})();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: <div className={staticClasses.Title}>Example Plugin</div>,
|
title: <div className={staticClasses.Title}>{about == null? "Kaylon": about.name}</div>,
|
||||||
content: <Content serverAPI={serverApi} />,
|
content: <Content serverAPI={serverApi} />,
|
||||||
icon: <GiWashingMachine />,
|
icon: <GiWashingMachine />,
|
||||||
onDismount() {
|
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>"
|
"NGnius (Graham) <ngniusness@gmail.com>"
|
||||||
],
|
],
|
||||||
"description": "Universal Steam Deck Plugin Library front-end designed for WASM",
|
"description": "Universal Steam Deck Plugin Library front-end designed for WASM",
|
||||||
"version": "0.6.0",
|
"version": "0.6.2",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"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
|
* Get the targeted plugin framework, or "any" if unknown
|
||||||
* @returns {string}
|
* @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.
|
* Call a function on the back-end.
|
||||||
* Returns null (None) if this fails for any reason.
|
* Returns null (None) if this fails for any reason.
|
||||||
|
@ -24,7 +42,10 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
|
||||||
export interface InitOutput {
|
export interface InitOutput {
|
||||||
readonly memory: WebAssembly.Memory;
|
readonly memory: WebAssembly.Memory;
|
||||||
readonly init_usdpl: (a: number) => void;
|
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 call_backend: (a: number, b: number, c: number, d: number) => number;
|
||||||
readonly __wbindgen_export_0: (a: number) => number;
|
readonly __wbindgen_export_0: (a: number) => number;
|
||||||
readonly __wbindgen_export_1: (a: number, b: number, c: 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 */
|
/* eslint-disable */
|
||||||
export const memory: WebAssembly.Memory;
|
export const memory: WebAssembly.Memory;
|
||||||
export function init_usdpl(a: number): void;
|
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 call_backend(a: number, b: number, c: number, d: number): number;
|
||||||
export function __wbindgen_export_0(a: number): number;
|
export function __wbindgen_export_0(a: number): number;
|
||||||
export function __wbindgen_export_1(a: number, b: number, c: number): number;
|
export function __wbindgen_export_1(a: number, b: number, c: number): number;
|
||||||
|
|
Reference in a new issue