From e42edddd33382855202ff2f17e6b77d65f6ad152 Mon Sep 17 00:00:00 2001 From: "Matthias P. Braendli" Date: Fri, 20 Sep 2024 11:23:53 +0200 Subject: Show RC params in dashboard --- Cargo.lock | 232 ++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/dabmux.rs | 112 +++++++++++++++++++++++ src/main.rs | 3 + src/ui.rs | 50 +++++++++- static/dashboard.js | 5 + static/settings.js | 41 +++++++++ static/style.css | 13 +++ templates/dashboard.html | 15 +++ templates/settings.html | 17 ++-- 10 files changed, 476 insertions(+), 13 deletions(-) create mode 100644 src/dabmux.rs create mode 100644 static/dashboard.js create mode 100644 static/settings.js diff --git a/Cargo.lock b/Cargo.lock index 0880ac8..0e47997 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -198,6 +198,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -257,9 +263,21 @@ version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" dependencies = [ + "jobserver", + "libc", "shlex", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -306,6 +324,62 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crypto-common" version = "0.1.6" @@ -341,6 +415,23 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dircpy" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a88521b0517f5f9d51d11925d8ab4523497dcf947073fa3231a311b63941131c" +dependencies = [ + "jwalk", + "log", + "walkdir", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "equivalent" version = "1.0.1" @@ -484,6 +575,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -640,6 +737,15 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.70" @@ -649,6 +755,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" +dependencies = [ + "crossbeam", + "rayon", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -811,6 +927,7 @@ dependencies = [ "toml", "tower-http", "tun", + "zmq", ] [[package]] @@ -880,6 +997,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "powerfmt" version = "0.2.0" @@ -943,13 +1066,33 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -970,6 +1113,15 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1136,6 +1288,25 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "thiserror" version = "1.0.63" @@ -1314,7 +1485,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bytes", "futures-util", "http", @@ -1455,12 +1626,28 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1522,6 +1709,15 @@ version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "windows" version = "0.51.1" @@ -1731,3 +1927,35 @@ dependencies = [ "quote", "syn 2.0.77", ] + +[[package]] +name = "zeromq-src" +version = "0.2.6+4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc120b771270365d5ed0dfb4baf1005f2243ae1ae83703265cb3504070f4160b" +dependencies = [ + "cc", + "dircpy", +] + +[[package]] +name = "zmq" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd3091dd571fb84a9b3e5e5c6a807d186c411c812c8618786c3c30e5349234e7" +dependencies = [ + "bitflags 1.3.2", + "libc", + "zmq-sys", +] + +[[package]] +name = "zmq-sys" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8351dc72494b4d7f5652a681c33634063bbad58046c1689e75270908fdc864" +dependencies = [ + "libc", + "system-deps", + "zeromq-src", +] diff --git a/Cargo.toml b/Cargo.toml index 2c3809c..a85e890 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ toml = "0.8" tokio = { version = "1", features = ["full"] } tokio-tungstenite = "0.21" tower-http = { version = "0.5.0", features = ["fs"] } +zmq = "0.10" futures-core = "0.3" futures= "0.3" diff --git a/src/dabmux.rs b/src/dabmux.rs new file mode 100644 index 0000000..fcdd864 --- /dev/null +++ b/src/dabmux.rs @@ -0,0 +1,112 @@ +use anyhow::anyhow; +use serde_json::Value; + +const ZMQ_TIMEOUT : i64 = 2000; + +pub struct DabMux { + ctx : zmq::Context, + rc_endpoint : String, +} + +pub struct Param { + pub module : String, + pub param : String, + pub value : String, +} + + +impl DabMux { + pub fn new() -> Self { + let ctx = zmq::Context::new(); + Self { + ctx, + rc_endpoint : "tcp://127.0.0.1:12722".to_owned() + } + } + + fn value_to_params(v: Value) -> anyhow::Result> { + let root = v.as_object().ok_or(anyhow!("RC data is not a JSON object"))?; + + let mut all_params = Vec::new(); + + for (module_name, params_value) in root { + let params = params_value.as_object().ok_or(anyhow!("RC module {} is not a JSON object", module_name))?; + + for (param_name, value_json) in params { + + let value = match value_json { + Value::Null => "null".to_owned(), + Value::Bool(b) => b.to_string(), + Value::Number(n) => n.to_string(), + Value::String(s) => s.clone(), + Value::Array(_) => return Err(anyhow!(format!("Unexpected array in {}.{}", module_name, param_name))), + Value::Object(_) => return Err(anyhow!(format!("Unexpected object in {}.{}", module_name, param_name))), + }; + + all_params.push( + Param { + module: module_name.to_owned(), + param: param_name.to_owned(), + value + }); + } + } + + Ok(all_params) + } + + pub fn get_rc_parameters(&mut self) -> anyhow::Result> { + let sock = self.ctx.socket(zmq::REQ)?; + sock.connect(&self.rc_endpoint)?; + sock.send("showjson", 0)?; + + let mut msg = zmq::Message::new(); + let mut items = [ + sock.as_poll_item(zmq::POLLIN), + ]; + zmq::poll(&mut items, ZMQ_TIMEOUT).unwrap(); + if items[0].is_readable() { + sock.recv(&mut msg, 0)?; + let msg = msg.as_str().ok_or(anyhow!("RC response is not a str"))?; + + // JSON structure: + // { "module1": { "param1": "value", "param2": "value" }, "module2": { ... } } + let v: Value = serde_json::from_str(msg)?; + Self::value_to_params(v) + } + else { + Err(anyhow!("Timeout reading RC")) + } + } + + pub fn set_rc_parameter(&mut self, module: &str, param: &str, value: &str) -> anyhow::Result { + let sock = self.ctx.socket(zmq::REQ)?; + sock.connect(&self.rc_endpoint)?; + sock.send_multipart(["set", module, param, value], 0)?; + + let mut items = [ + sock.as_poll_item(zmq::POLLIN), + ]; + zmq::poll(&mut items, ZMQ_TIMEOUT).unwrap(); + if items[0].is_readable() { + let mut parts = sock.recv_multipart(0)?; + + let j : String = parts.drain(..) + .map(|p| match String::from_utf8(p) + { + Ok(s) => s, + Err(_) => "???".to_owned(), + }) + .collect::>() + .join(","); + + eprintln!("SET_RC: {}", j); + + let v: Value = serde_json::Value::String(j); + Ok(v) + } + else { + Err(anyhow!("Timeout reading RC")) + } + } +} diff --git a/src/main.rs b/src/main.rs index e64570a..66fbe23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,11 @@ use log::{debug, info, warn, error}; mod ui; mod config; +mod dabmux; struct AppState { conf : config::Config, + dabmux : dabmux::DabMux, } type SharedState = Arc>; @@ -22,6 +24,7 @@ async fn main() -> std::io::Result<()> { let shared_state = Arc::new(Mutex::new(AppState { conf : conf.clone(), + dabmux : dabmux::DabMux::new(), })); let port = 3000; diff --git a/src/ui.rs b/src/ui.rs index 914d289..6bcb04d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -24,6 +24,7 @@ pub async fn serve(port: u16, shared_state: SharedState) { .route("/", get(dashboard)) .route("/settings", get(show_settings)) .route("/api/settings", post(post_settings)) + .route("/api/set_rc", post(post_rc)) .nest_service("/static", ServeDir::new("static")) /* For an example for timeouts and tracing, have a look at the git history */ .with_state(shared_state); @@ -45,7 +46,7 @@ impl ActivePage { // Used by templates/head.html to include the correct js files in fn styles(&self) -> Vec<&'static str> { match self { - ActivePage::Dashboard => vec![], + ActivePage::Dashboard => vec!["dashboard.js", "main.js"], ActivePage::Settings => vec!["settings.js", "main.js"], ActivePage::None => vec![], } @@ -58,18 +59,59 @@ struct DashboardTemplate<'a> { title: &'a str, page: ActivePage, conf: config::Config, + errors: Option, + params: Vec, } async fn dashboard(State(state): State) -> DashboardTemplate<'static> { - let conf = { - let st = state.lock().unwrap(); - st.conf.clone() + let (conf, params_result) = { + let mut st = state.lock().unwrap(); + + let params_result = st.dabmux.get_rc_parameters(); + + (st.conf.clone(), params_result) + }; + + let (params, errors) = match params_result { + Ok(v) => { + (v, None) + }, + Err(e) => { + (Vec::new(), Some(format!("{}", e))) + }, }; DashboardTemplate { title: "Dashboard", conf, page: ActivePage::Dashboard, + params, + errors, + } +} + +#[derive(Deserialize)] +struct SetRc { + pub module : String, + pub param : String, + pub value : String, +} + +async fn post_rc( + State(state): State, + Json(set_rc): Json) -> (StatusCode, Json) { + + let set_rc_result = { + let mut st = state.lock().unwrap(); + st.dabmux.set_rc_parameter(&set_rc.module, &set_rc.param, &set_rc.value) + }; + + match set_rc_result { + Ok(v) => (StatusCode::OK, Json(v)), + Err(e) => { + let e_str = serde_json::Value::String(e.to_string()); + (StatusCode::BAD_REQUEST, Json(e_str)) + }, } } diff --git a/static/dashboard.js b/static/dashboard.js new file mode 100644 index 0000000..6eb8274 --- /dev/null +++ b/static/dashboard.js @@ -0,0 +1,5 @@ +async function btn_dash_update(element_clicked, module, param) { + let value = element_clicked.parentElement.children[0].value; + let data = {'module': module, 'param': param, 'value': value}; + await post('/api/set_rc', data); +} diff --git a/static/settings.js b/static/settings.js new file mode 100644 index 0000000..287f6f6 --- /dev/null +++ b/static/settings.js @@ -0,0 +1,41 @@ +async function btn_settings_add_service() { + const template = document.getElementById('service_template'); + + let clon = template.content.cloneNode(true); + document.getElementById('services').appendChild(clon); +} + +async function btn_settings_remove_service(element_clicked) { + element_clicked.parentElement.remove() +} + +async function btn_settings_send() { + let data = { + 'instance_name': document.getElementById('instance_name').value, + 'tist': document.getElementById('tist').checked, + 'tist_offset': parseInt(document.getElementById('tist_offset').value, 10), + 'ensemble_id': parseInt(document.getElementById('ensemble_id').value, 16), + 'ensemble_ecc': parseInt(document.getElementById('ensemble_ecc').value, 16), + 'ensemble_label': document.getElementById('ensemble_label').value, + 'ensemble_shortlabel': document.getElementById('ensemble_shortlabel').value, + 'output_edi_port': parseInt(document.getElementById('output_edi_port').value, 10), + 'services': [], + }; + + const services = document.getElementById('services'); + const destList = services.querySelectorAll("p.service"); + for (let i = 0; i < destList.length; i++) { + data.services.push({ + 'sid': parseInt(destList[i].querySelector("input.srv_sid").value, 16), + 'ecc': parseInt(destList[i].querySelector("input.srv_ecc").value, 16), + 'label': destList[i].querySelector("input.srv_label").value, + 'shortlabel': destList[i].querySelector("input.srv_shortlabel").value, + 'input_port': parseInt(destList[i].querySelector("input.srv_input_port").value, 10), + 'bitrate': parseInt(destList[i].querySelector("input.srv_bitrate").value, 10), + 'protection': parseInt(destList[i].querySelector("input.srv_protection").value, 10), + }); + } + + await post('/api/settings', data); +} + diff --git a/static/style.css b/static/style.css index dd43c7a..39ca359 100644 --- a/static/style.css +++ b/static/style.css @@ -29,6 +29,19 @@ nav { flex: none; } +.section { + padding-left: 0.25rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.setting-entry { + padding-left: 0.25rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + + .div-menu { border-top-width: 1px; border-bottom-width: 0px; diff --git a/templates/dashboard.html b/templates/dashboard.html index c8a1693..040538c 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -7,6 +7,21 @@

Remote control

+ + {% if let Some(e) = errors %} +

Error!: {{ e }}

+ {% endif %} + + + + {% for p in params %} + + + {% endfor %} +
ModuleParameterValue
{{ p.module }}{{ p.param }} + + +
{% include "foot.html" %} diff --git a/templates/settings.html b/templates/settings.html index dfe71cf..ca9a77a 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -3,34 +3,37 @@

ODR-DabMux Settings

General

-
-
+
+ +
+
-
+
-
+
-
+
-
+
-
+
+

Services: