Improve scalability and documentation

This commit is contained in:
NGnius (Graham) 2022-09-12 21:08:50 -04:00
parent 569eab5880
commit 79f730b9c6
26 changed files with 465 additions and 162 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 NGnius
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

83
backend/Cargo.lock generated
View file

@ -88,6 +88,42 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd"
dependencies = [
"bitflags",
"clap_derive",
"clap_lex",
"indexmap",
"once_cell",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "cpufeatures"
version = "0.2.2"
@ -264,6 +300,12 @@ dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -372,6 +414,7 @@ name = "kaylon"
version = "0.1.0"
dependencies = [
"async-trait",
"clap",
"log",
"serde",
"serde_json",
@ -484,6 +527,12 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "os_str_bytes"
version = "6.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
[[package]]
name = "percent-encoding"
version = "2.1.0"
@ -528,6 +577,30 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.40"
@ -749,6 +822,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thiserror"
version = "1.0.31"
@ -984,6 +1063,8 @@ dependencies = [
[[package]]
name = "usdpl-back"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca96dac4ee471e9534940f99cb36f5212cbfaf4e7779eb3ba970d3c511d9583"
dependencies = [
"async-recursion",
"async-trait",
@ -997,6 +1078,8 @@ dependencies = [
[[package]]
name = "usdpl-core"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "862153581fac266458521f49e5906a71c1eee1665cb4c7d71e9586bd34b45394"
dependencies = [
"base64",
]

View file

@ -1,15 +1,19 @@
[package]
name = "kaylon" # TODO replace with plugin name (also in build.sh)
name = "kaylon"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
authors = ["NGnius <ngniusness@gmail.com>"]
description = "Better than the Borg"
license = "MIT"
repository = "https://github.com/NGnius/kaylon"
[dependencies]
usdpl-back = { version = "0.7.0", features = ["decky"], path = "../../usdpl-rs/usdpl-back" }
usdpl-back = { version = "0.7.0", features = ["decky"]}
clap = { version = "3.2", features = ["derive", "std"], default-features = false }
# async
tokio = { version = "*", features = ["sync", "time"] }
tokio = { version = "*", features = ["time"] }
async-trait = "0.1.57"
# json

View file

@ -4,6 +4,7 @@ use super::ApiParameterType;
use crate::runtime::{QueueAction, QueueItem};
/// API web method to retrieve AboutConfig from the back-end, as described in the config file
pub fn get_about(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
let sender = Mutex::new(sender);
move |_| {

View file

@ -0,0 +1,19 @@
use std::sync::mpsc::{TryRecvError, Receiver};
use std::sync::Mutex;
/// Receive on a blocking channel in an async manner (by polling conservatively)
pub async fn channel_recv<T>(rx: Receiver<T>) -> Result<T, TryRecvError> {
let sleep_duration = std::time::Duration::from_millis(10);
let receiver = Mutex::new(rx);
loop {
let received = receiver.lock().unwrap().try_recv();
match received {
Err(TryRecvError::Disconnected) => {
return Err(TryRecvError::Disconnected);
},
Err(_) => {},
Ok(x) => return Ok(x),
}
tokio::time::sleep(sleep_duration).await;
}
}

View file

@ -3,10 +3,12 @@ use std::sync::{Mutex, mpsc::{Sender, channel, self}};
use usdpl_back::core::serdes::Primitive;
use usdpl_back::AsyncCallable;
use super::ApiParameterType;
use super::{ApiParameterType, ApiDisplayResult};
use crate::runtime::{QueueAction, QueueItem};
/// API web method to retrieve the latest display result for an element,
// or wait for the next display result if no display result is cached
pub struct GetDisplayEndpoint {
//sender: tokio::sync::mpsc::Sender<SetCallbackAsync>,
//receiver: Mutex<Option<tokio::sync::mpsc::Receiver<SetCallbackAsync>>>,
@ -40,33 +42,31 @@ impl AsyncCallable for GetDisplayEndpoint {
}
}
);
if let Ok(_) = send_result {
match send_result {
Ok(_) => {
// 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 {
match super::async_utils::channel_recv(receiver).await {
Err(mpsc::TryRecvError::Disconnected) => {
log::info!("Failed to response for get_display for #{}", index);
return vec![Primitive::Empty];
let msg = format!("Failed to response for get_display for #{}", index);
log::warn!("{}", msg);
return vec![ApiDisplayResult::failure(msg, "receiving channel disconnected").to_primitive()];
},
Err(_) => {},
Err(_) => return vec![], // impossible
Ok(x) => {
log::debug!("got display for item #{}", index);
return vec![x];
return vec![ApiDisplayResult::success(x).to_primitive()];
},
}
tokio::time::sleep(sleep_duration).await;
},
Err(_e) => {
let msg = format!("Failed to get_display for #{}", index);
log::warn!("{}", msg);
vec![ApiDisplayResult::failure(msg, "sending channel disconnected").to_primitive()]
}
}
} else {
log::info!("Failed to get_display for #{}", index);
vec![Primitive::Empty]
}
} else {
vec![Primitive::Empty]
vec![ApiDisplayResult::failure("Failed to get param 0", "invalid call parameters").to_primitive()]
}
}
}

View file

@ -4,6 +4,7 @@ use super::ApiParameterType;
use crate::runtime::{QueueAction, QueueItem};
/// API web method to retrieve all ElementConfig items from the back-end, as described in the config file
pub fn get_items(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
let sender = Mutex::new(sender);
move |_| {

View file

@ -1,13 +1,16 @@
mod about;
pub(crate) mod async_utils;
mod get_display;
mod get_item;
mod get_items;
mod on_update;
mod reload;
mod types;
pub use about::get_about;
pub use get_display::GetDisplayEndpoint;
pub use get_item::get_items;
pub use get_items::get_items;
pub use on_update::on_update;
pub use reload::reload;
pub(super) use types::*;
pub(super) type ApiParameterType = Vec<usdpl_back::core::serdes::Primitive>;

View file

@ -6,6 +6,7 @@ use super::ApiParameterType;
use crate::runtime::{QueueAction, QueueItem};
/// API web method to notify the back-end of an update event (e.g. click, slider slide, toggle)
pub fn on_update(sender: Sender<QueueItem>) -> impl Fn(ApiParameterType) -> ApiParameterType {
let sender = Mutex::new(sender);
move |mut params: ApiParameterType| {

70
backend/src/api/types.rs Normal file
View file

@ -0,0 +1,70 @@
use serde::{Serialize, Deserialize};
use serde_json::Value;
use usdpl_back::core::serdes::Primitive;
#[derive(Serialize, Deserialize, Clone)]
#[serde(tag = "element")]
pub enum ApiDisplayResult {
#[serde(rename = "value")]
Value(ApiValue),
#[serde(rename = "error")]
Error(ApiError),
}
impl ApiDisplayResult {
pub fn dump(&self) -> String {
serde_json::to_string(self).unwrap()
}
pub fn to_primitive(self) -> Primitive {
Primitive::Json(self.dump())
}
pub fn success(primitive: Primitive) -> Self {
Self::Value(ApiValue::new(primitive))
}
pub fn failure<S: Into<String>, D: core::fmt::Display>(msg: S, err: D) -> Self {
Self::Error(ApiError::new(msg, err))
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ApiValue {
pub value: Value,
}
impl ApiValue {
pub fn new(primitive: Primitive) -> Self {
let val = match primitive {
Primitive::Empty => Value::Null,
Primitive::String(s) => Value::String(s),
Primitive::F32(x) => x.into(),
Primitive::F64(x) => x.into(),
Primitive::U32(x) => Value::Number(x.into()),
Primitive::U64(x) => Value::Number(x.into()),
Primitive::I32(x) => Value::Number(x.into()),
Primitive::I64(x) => Value::Number(x.into()),
Primitive::Bool(x) => Value::Bool(x),
Primitive::Json(x) => serde_json::from_str(&x).unwrap_or(Value::Null),
};
Self {
value: val,
}
}
}
#[derive(Serialize, Deserialize, Clone)]
pub struct ApiError {
pub message: String,
pub exception: String,
}
impl ApiError {
pub fn new<S: Into<String>, D: core::fmt::Display>(msg: S, err: D) -> Self {
Self {
message: msg.into(),
exception: err.to_string(),
}
}
}

19
backend/src/cli.rs Normal file
View file

@ -0,0 +1,19 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
pub struct CliArgs {
/// Path to config file
#[clap(long)]
pub config: Option<std::path::PathBuf>,
/// Path to log file
#[clap(long)]
pub log: Option<std::path::PathBuf>,
}
impl CliArgs {
pub fn cli() -> Self {
Self::parse()
}
}

View file

@ -1,4 +1,5 @@
mod api;
mod cli;
mod config;
mod consts;
mod runtime;
@ -9,16 +10,19 @@ use usdpl_back::Instance;
use usdpl_back::core::serdes::Primitive;
fn main() -> Result<(), ()> {
let log_filepath = format!("/tmp/{}.log", consts::PACKAGE_NAME);
let cli_args = cli::CliArgs::cli();
let log_filepath = cli_args.log.unwrap_or_else(|| format!("/tmp/{}.log", consts::PACKAGE_NAME).into());
WriteLogger::init(
#[cfg(debug_assertions)]{LevelFilter::Debug},
#[cfg(not(debug_assertions))]{LevelFilter::Info},
Default::default(),
std::fs::File::create(&log_filepath).unwrap()
std::fs::File::create(log_filepath).unwrap()
).unwrap();
let kaylon_conf = config::BaseConfig::load(consts::FILEPATH);
let (executor, sender) = runtime::RuntimeExecutor::new(kaylon_conf);
let filepath = cli_args.config.unwrap_or(consts::FILEPATH.into());
let kaylon_conf = config::BaseConfig::load(&filepath);
let (executor, sender) = runtime::RuntimeExecutor::new(kaylon_conf, filepath);
log::info!("Starting back-end ({} v{})", consts::PACKAGE_NAME, consts::PACKAGE_VERSION);
println!("Starting back-end ({} v{})", consts::PACKAGE_NAME, consts::PACKAGE_VERSION);

View file

@ -4,6 +4,7 @@ use crate::config::{ElementConfig, ActionConfig};
pub type ActError = String;
/// Something capable of performing an action.
pub trait Act: Sized {
type Param;
type Config: ?Sized;
@ -12,6 +13,7 @@ pub trait Act: Sized {
fn run(self) -> Self::Return;
}
/// Action performer for a regular element
pub struct Actor {
actor_type: ActorType,
index: usize,

View file

@ -7,6 +7,7 @@ use super::{Act, ActError};
const VALUE_ENV_VAR: &str = "KAYLON_VALUE";
/// Runs a CLI command in Bash
pub struct CommandActor {
shell: String,
run: String,

View file

@ -4,6 +4,7 @@ use usdpl_back::core::serdes::Primitive;
use crate::config::{AboutConfig, ElementConfig};
/// An API operation for the executor to perform
pub enum QueueAction {
GetAbout {
respond_to: Sender<AboutConfig>,
@ -21,6 +22,7 @@ pub enum QueueAction {
}
}
/// Wrapper for an executor command
pub struct QueueItem {
pub action: QueueAction,
}

View file

@ -1,4 +1,5 @@
use std::thread;
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, Sender};
use crate::config::{BaseConfig, ElementConfig};
@ -7,15 +8,17 @@ use super::{ResultRouter, RouterCommand};
pub struct RuntimeExecutor {
config_data: BaseConfig,
tasks_receiver: Receiver<QueueItem>
tasks_receiver: Receiver<QueueItem>,
config_path: PathBuf,
}
impl RuntimeExecutor {
pub fn new(conf: BaseConfig) -> (Self, Sender<QueueItem>) {
pub fn new<P: AsRef<Path>>(conf: BaseConfig, path: P) -> (Self, Sender<QueueItem>) {
let (tx, rx) = mpsc::channel();
(Self {
config_data: conf,
tasks_receiver: rx,
config_path: path.as_ref().to_path_buf(),
}, tx)
}
@ -36,6 +39,7 @@ impl RuntimeExecutor {
ExecutorState {
result_handler: ExecutorState::build_router(self.config_data.items().len()),
config_data: self.config_data,
config_path: self.config_path,
},
self.tasks_receiver
)
@ -45,15 +49,19 @@ impl RuntimeExecutor {
struct ExecutorState {
config_data: BaseConfig,
result_handler: Sender<RouterCommand>,
config_path: PathBuf,
}
impl ExecutorState {
fn handle_item(&mut self, item: QueueItem) {
match item.action {
QueueAction::GetAbout { respond_to } => {
// retrieve about information from (in-memory) config file
respond_to.send(self.config_data.get_about().clone()).unwrap_or(());
},
QueueAction::DoUpdate { index, value } => {
// trigger update event for element
// i.e. on_click, on_toggle, etc. action
if let Some(item) = self.config_data.get_item(index) {
match super::Actor::build(item, (index, value)) {
Ok(act) => {
@ -73,23 +81,30 @@ impl ExecutorState {
}
},
QueueAction::DoReload { respond_to } => {
self.config_data = BaseConfig::load(crate::consts::FILEPATH);
// reload config file from storage
self.config_data = BaseConfig::load(&self.config_path);
self.populate_router();
respond_to.send(self.config_data.items().clone()).unwrap_or(());
},
QueueAction::SetCallback { index, respond_to } => {
// register a callback with the ResultRouter for an element's action
// the next time that action is performed, the result will be sent through the callback
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(
if let Err(e) = 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());
if let Err(_) = self.result_handler.send(e.0) {
// don't retry if another error occurs
log::error!("Failed to send to ResultRouter again, did not SetCallback for item #{}", index);
}
}
}
}

View file

@ -6,6 +6,7 @@ use usdpl_back::core::serdes::Primitive;
use crate::config::ReadingConfig;
use super::{Act, ActError, ActorType, RouterCommand};
/// Runs an action periodically
pub struct PeriodicActor {
config: ReadingConfig,
result_handler: Sender<RouterCommand>,

View file

@ -20,9 +20,13 @@ pub enum RouterCommand {
}
pub struct ResultRouter {
/// receiver for new router commands to perform
comm: Receiver<RouterCommand>,
/// active callbacks; more than one sender may listen for a result
senders: Vec<[Option<Sender<Primitive>>; MAX_HANDLERS_PER_ITEM]>,
/// cache of sender, for Act paradigm
comm_tx: Option<Sender<RouterCommand>>,
/// cache of unheard results
cache: Vec<Option<Primitive>>,
}
@ -62,16 +66,21 @@ impl Act for ResultRouter {
for command in self.comm.iter() {
match command {
RouterCommand::AddSender { index, sender } => {
// register result listener
log::debug!("Handling AddSender for item #{}", index);
if let Some(senders) = self.senders.get_mut(index) {
// send cached value, if available
// send cached value, if available.
// This avoids race conditions from a result being received before
// a listener has been registered. This is especially an issue during
// program start, when actions run immediately and listeners come from
// the slow front-end (web request in the browser)
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);
self.cache[index] = Some(e.0); // re-cache if send fails
log::debug!("ResultRouter ignoring AddSender since sending cached value failed");
continue;
},
@ -94,9 +103,11 @@ impl Act for ResultRouter {
}
}
RouterCommand::HandleResult {index, result} => {
// send a result to all (relevant) listeners
log::debug!("Handling HandleResult for item #{}", index);
if let Some(senders) = self.senders.get_mut(index) {
if Self::all_senders_none(senders) {
// cache result if it won't be heard
self.cache[index] = Some(result);
log::debug!("Cached result for item #{}", index);
} else {

View file

@ -1,47 +1,62 @@
{
"api-version": "v0.0.0",
"items": [
{
"element": "result-display",
"title": "Best Kaylon",
"result_of": 1
},
{
"element": "button",
"title": "Test Button",
"title": "Tell Me!",
"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'"
"run": "echo ISAAC"
}
},
{
"element": "result-display",
"title": "Test Result",
"title": "Smartest Kaylon",
"result_of": 1
},
{
"element": "toggle",
"title": "Switch",
"description": "Toggle me!",
"on_toggle": {
"action": "command",
"run": "echo \"Toggle is now ${KAYLON_VALUE}\""
}
},
{
"element": "result-display",
"title": "",
"result_of": 3
},
{
"element": "slider",
"title": "Slider",
"min": 0,
"max": 10,
"notches": null,
"on_set": {
"action": "command",
"run": "echo \"Slider is now ${KAYLON_VALUE}\""
}
},
{
"element": "result-display",
"title": "",
"result_of": 5
},
{
"element": "reading",
"title": "Fan Speed",
"period_ms": 1000,
"on_period": {
"action": "command",
"run": "cat /sys/class/hwmon/hwmon5/fan1_input"
}
}
],
"about": {

View file

@ -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", "--config", ""])
while True:
await asyncio.sleep(1)

171
package-lock.json generated
View file

@ -9,7 +9,7 @@
"version": "0.0.1",
"license": "GPL-2.0-or-later",
"dependencies": {
"decky-frontend-lib": "^1.0.1",
"decky-frontend-lib": "*",
"react-icons": "^4.3.1",
"usdpl-front": "file:./src/usdpl_front"
},
@ -77,9 +77,9 @@
"dev": true
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"version": "0.3.15",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz",
"integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==",
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.0.3",
@ -153,9 +153,9 @@
}
},
"node_modules/@rollup/plugin-typescript": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.4.tgz",
"integrity": "sha512-wt7JnYE9antX6BOXtsxGoeVSu4dZfw0dU3xykfOQ4hC3EddxRbVG/K0xiY1Wup7QOHJcjLYXWAn0Kx9Z1SBHHg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz",
"integrity": "sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^3.1.0",
@ -199,9 +199,9 @@
"dev": true
},
"node_modules/@types/eslint": {
"version": "8.4.5",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz",
"integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==",
"version": "8.4.6",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz",
"integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==",
"dev": true,
"dependencies": {
"@types/estree": "*",
@ -231,9 +231,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "18.6.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.4.tgz",
"integrity": "sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==",
"version": "18.7.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.17.tgz",
"integrity": "sha512-0UyfUnt02zIuqp7yC8RYtDkp/vo8bFaQ13KkSEvUAohPOAlnVNbj5Fi3fgPSuwzakS+EvvnnZ4x9y7i6ASaSPQ==",
"dev": true
},
"node_modules/@types/prop-types": {
@ -539,9 +539,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001374",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz",
"integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==",
"version": "1.0.30001397",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz",
"integrity": "sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA==",
"dev": true,
"funding": [
{
@ -588,9 +588,12 @@
"dev": true
},
"node_modules/decky-frontend-lib": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/decky-frontend-lib/-/decky-frontend-lib-1.3.0.tgz",
"integrity": "sha512-j3MfgfSlAE1TUZ5BCmwh01GLFUZsCkDH/FRfTWGYbaJ+49sy6WM9u/1hFHTuX43lK9ZM4BABvweKjsi2AnzsMw=="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/decky-frontend-lib/-/decky-frontend-lib-3.0.0.tgz",
"integrity": "sha512-ZqJ9ii7QoYWHFfkU8hV82IHi3+McZDmE4wS22duXpgRI8r5BBMiZItw6tYkc24ZtsJIVso83FFt7adcEBqBwJA==",
"dependencies": {
"minimist": "^1.2.6"
}
},
"node_modules/deepmerge": {
"version": "4.2.2",
@ -602,9 +605,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.211",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.211.tgz",
"integrity": "sha512-BZSbMpyFQU0KBJ1JG26XGeFI3i4op+qOYGxftmZXFZoHkhLgsSv4DHDJfl8ogII3hIuzGt51PaZ195OVu0yJ9A==",
"version": "1.4.248",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.248.tgz",
"integrity": "sha512-qShjzEYpa57NnhbW2K+g+Fl+eNoDvQ7I+2MRwWnU6Z6F0HhXekzsECCLv+y2OJUsRodjqoSfwHkIX42VUFtUzg==",
"dev": true
},
"node_modules/enhanced-resolve": {
@ -955,8 +958,7 @@
"node_modules/minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"node_modules/neo-async": {
"version": "2.6.2",
@ -1080,9 +1082,9 @@
}
},
"node_modules/rollup": {
"version": "2.77.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.2.tgz",
"integrity": "sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g==",
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.0.tgz",
"integrity": "sha512-x4KsrCgwQ7ZJPcFA/SUu6QVcYlO7uRLfLAy0DSA4NS2eG8japdbpM50ToH7z4iObodRYOJ0soneF0iaQRJ6zhA==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
@ -1264,9 +1266,9 @@
}
},
"node_modules/terser": {
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz",
"integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==",
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.2",
@ -1282,16 +1284,16 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz",
"integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==",
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz",
"integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.7",
"@jridgewell/trace-mapping": "^0.3.14",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.0",
"terser": "^5.7.2"
"terser": "^5.14.1"
},
"engines": {
"node": ">= 10.13.0"
@ -1322,9 +1324,9 @@
"dev": true
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz",
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@ -1335,9 +1337,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz",
"integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.8.tgz",
"integrity": "sha512-GHg7C4M7oJSJYW/ED/5QOJ7nL/E0lwTOBGsOorA7jqHr8ExUhPfwAotIAmdSw/LWv3SMLSNpzTAgeLG9zaZKTA==",
"dev": true,
"funding": [
{
@ -1461,11 +1463,8 @@
"dev": true
},
"src/usdpl_front": {
"version": "0.6.0",
"version": "0.6.2",
"license": "GPL-3.0-only"
},
"src/usdpl-front": {
"extraneous": true
}
},
"dependencies": {
@ -1509,9 +1508,9 @@
"dev": true
},
"@jridgewell/trace-mapping": {
"version": "0.3.14",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
"integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
"version": "0.3.15",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz",
"integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "^3.0.3",
@ -1567,9 +1566,9 @@
}
},
"@rollup/plugin-typescript": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.4.tgz",
"integrity": "sha512-wt7JnYE9antX6BOXtsxGoeVSu4dZfw0dU3xykfOQ4hC3EddxRbVG/K0xiY1Wup7QOHJcjLYXWAn0Kx9Z1SBHHg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz",
"integrity": "sha512-wMv1/scv0m/rXx21wD2IsBbJFba8wGF3ErJIr6IKRfRj49S85Lszbxb4DCo8iILpluTjk2GAAu9CoZt4G3ppgQ==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
@ -1596,9 +1595,9 @@
}
},
"@types/eslint": {
"version": "8.4.5",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.5.tgz",
"integrity": "sha512-dhsC09y1gpJWnK+Ff4SGvCuSnk9DaU0BJZSzOwa6GVSg65XtTugLBITDAAzRU5duGBoXBHpdR/9jHGxJjNflJQ==",
"version": "8.4.6",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.6.tgz",
"integrity": "sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g==",
"dev": true,
"requires": {
"@types/estree": "*",
@ -1628,9 +1627,9 @@
"dev": true
},
"@types/node": {
"version": "18.6.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.4.tgz",
"integrity": "sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==",
"version": "18.7.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.17.tgz",
"integrity": "sha512-0UyfUnt02zIuqp7yC8RYtDkp/vo8bFaQ13KkSEvUAohPOAlnVNbj5Fi3fgPSuwzakS+EvvnnZ4x9y7i6ASaSPQ==",
"dev": true
},
"@types/prop-types": {
@ -1900,9 +1899,9 @@
"dev": true
},
"caniuse-lite": {
"version": "1.0.30001374",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001374.tgz",
"integrity": "sha512-mWvzatRx3w+j5wx/mpFN5v5twlPrabG8NqX2c6e45LCpymdoGqNvRkRutFUqpRTXKFQFNQJasvK0YT7suW6/Hw==",
"version": "1.0.30001397",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001397.tgz",
"integrity": "sha512-SW9N2TbCdLf0eiNDRrrQXx2sOkaakNZbCjgNpPyMJJbiOrU5QzMIrXOVMRM1myBXTD5iTkdrtU/EguCrBocHlA==",
"dev": true
},
"chrome-trace-event": {
@ -1936,9 +1935,12 @@
"dev": true
},
"decky-frontend-lib": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/decky-frontend-lib/-/decky-frontend-lib-1.3.0.tgz",
"integrity": "sha512-j3MfgfSlAE1TUZ5BCmwh01GLFUZsCkDH/FRfTWGYbaJ+49sy6WM9u/1hFHTuX43lK9ZM4BABvweKjsi2AnzsMw=="
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/decky-frontend-lib/-/decky-frontend-lib-3.0.0.tgz",
"integrity": "sha512-ZqJ9ii7QoYWHFfkU8hV82IHi3+McZDmE4wS22duXpgRI8r5BBMiZItw6tYkc24ZtsJIVso83FFt7adcEBqBwJA==",
"requires": {
"minimist": "^1.2.6"
}
},
"deepmerge": {
"version": "4.2.2",
@ -1947,9 +1949,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.4.211",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.211.tgz",
"integrity": "sha512-BZSbMpyFQU0KBJ1JG26XGeFI3i4op+qOYGxftmZXFZoHkhLgsSv4DHDJfl8ogII3hIuzGt51PaZ195OVu0yJ9A==",
"version": "1.4.248",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.248.tgz",
"integrity": "sha512-qShjzEYpa57NnhbW2K+g+Fl+eNoDvQ7I+2MRwWnU6Z6F0HhXekzsECCLv+y2OJUsRodjqoSfwHkIX42VUFtUzg==",
"dev": true
},
"enhanced-resolve": {
@ -2232,8 +2234,7 @@
"minimist": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
"dev": true
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"neo-async": {
"version": "2.6.2",
@ -2331,9 +2332,9 @@
}
},
"rollup": {
"version": "2.77.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.2.tgz",
"integrity": "sha512-m/4YzYgLcpMQbxX3NmAqDvwLATZzxt8bIegO78FZLl+lAgKJBd1DRAOeEiZcKOIOPjxE6ewHWHNgGEalFXuz1g==",
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.0.tgz",
"integrity": "sha512-x4KsrCgwQ7ZJPcFA/SUu6QVcYlO7uRLfLAy0DSA4NS2eG8japdbpM50ToH7z4iObodRYOJ0soneF0iaQRJ6zhA==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
@ -2457,9 +2458,9 @@
"dev": true
},
"terser": {
"version": "5.14.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz",
"integrity": "sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz",
"integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==",
"dev": true,
"requires": {
"@jridgewell/source-map": "^0.3.2",
@ -2469,16 +2470,16 @@
}
},
"terser-webpack-plugin": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.3.tgz",
"integrity": "sha512-Fx60G5HNYknNTNQnzQ1VePRuu89ZVYWfjRAeT5rITuCY/1b08s49e5kSQwHDirKZWuoKOBRFS98EUUoZ9kLEwQ==",
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz",
"integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==",
"dev": true,
"requires": {
"@jridgewell/trace-mapping": "^0.3.7",
"@jridgewell/trace-mapping": "^0.3.14",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.0",
"terser": "^5.7.2"
"terser": "^5.14.1"
}
},
"tslib": {
@ -2488,15 +2489,15 @@
"dev": true
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz",
"integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==",
"dev": true
},
"update-browserslist-db": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz",
"integrity": "sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.8.tgz",
"integrity": "sha512-GHg7C4M7oJSJYW/ED/5QOJ7nL/E0lwTOBGsOorA7jqHr8ExUhPfwAotIAmdSw/LWv3SMLSNpzTAgeLG9zaZKTA==",
"dev": true,
"requires": {
"escalade": "^3.1.1",

View file

@ -1,9 +1,9 @@
{
"name": "Kaylon",
"author": "NGnius",
"flags": ["debug", "_root"],
"flags": ["_debug", "_root"],
"publish": {
"tags": ["template", "root"],
"tags": ["automation", "cli"],
"description": "Better than the Borg",
"image": ""
}

View file

@ -66,6 +66,19 @@ export type CResultDisplay = {
export type CElement = CButton | CToggle | CSlider | CReading | CResultDisplay;
export type CErrorResult = {
element: string; // "error"
message: string;
exception: string;
}
export type CValueResult = {
element: string; // "value"
value: any;
}
export type CDisplayResponse = CValueResult | CErrorResult;
export async function getElements(): Promise<CElement[]> {
return (await call_backend("get_items", []))[0];
}
@ -74,7 +87,7 @@ export async function onUpdate(index: number, value: any): Promise<any> {
return (await call_backend("on_update", [index, value]))[0];
}
export async function getDisplay(index: number): Promise<string | null> {
export async function getDisplay(index: number): Promise<CDisplayResponse> {
return (await call_backend("get_display", [index]))[0];
}

View file

@ -33,10 +33,26 @@ let about: backend.CAbout | null = null;
let update = () => {};
function displayCallback(index: number) {
return (newVal: any) => {
set_value(DISPLAY_KEY + index.toString(), newVal);
return (newVal: backend.CDisplayResponse) => {
if (newVal != null) {
switch (newVal.element) {
case "value":
let val = newVal as backend.CValueResult;
console.log("KAYLON: Got display for " + index.toString(), val);
set_value(DISPLAY_KEY + index.toString(), val.value);
break;
case "error":
let err = newVal as backend.CErrorResult;
console.warn("KAYLON: Got display error for " + index.toString(), err);
break;
default:
console.error("KAYLON: Got invalid display response for " + index.toString(), newVal);
break;
}
} else {
console.warn("KAYLON: Ignoring null display result for " + index.toString());
}
backend.resolve(backend.getDisplay(index), displayCallback(index));
console.log("Got display for " + index.toString(), newVal);
update();
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.