diff --git a/public/global.css b/public/global.css index 5da789b..faf38f0 100644 --- a/public/global.css +++ b/public/global.css @@ -12,7 +12,7 @@ body { color: #ddd; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - font-size: 1rem; + /* font-size: 1rem; */ user-select: none; } @@ -23,20 +23,49 @@ pre { body { margin: 0; padding: 1rem; + border: 0.125rem solid #333; } +/* titlebar */ + +.titlebar { + user-select: none; + background: #333; + display: flex; + align-items: center; + justify-content: flex-start; + position: fixed; + top: 0; + left: 0; + right: 0; + padding: 0.25rem 0.5rem; + height: 2rem; +} + +.header-icon { + max-height: 100%; +} + +.header-icon img { + max-height: 1.5rem; + pointer-events: none; +} + +.header { + font-size: 1.5rem; + font-weight: 500; +} + +/* main */ + .main { + margin-top: 2rem; display: flex; flex-flow: column nowrap; align-items: stretch; justify-content: flex-start; } -.header { - font-size: 2rem; - font-weight: 500; -} - .row, .row-2 { margin: 0 0 0.5rem 0; @@ -50,6 +79,14 @@ body { flex: 1 1 0; } +.row .label[title] { + text-decoration: underline dotted; +} + +.row .label[title]:hover { + text-decoration: underline; +} + .row .input { flex: 2 2 0; } @@ -58,9 +95,7 @@ body { width: 100%; max-height: 5rem; overflow-x: auto; -} -.serverlist > pre { - font-size: 0.75em; + font-size: 0.75rem; } input, @@ -69,6 +104,7 @@ select { background-color: #444; color: #ddd; border: none; + font-size: 1rem; } button { @@ -92,6 +128,8 @@ button.primary { background: rgb(35, 67, 211); } +/* Preview */ + .preview { width: 100%; display: flex; @@ -107,6 +145,9 @@ button.primary { flex-flow: column-reverse nowrap; align-items: stretch; justify-content: flex-start; + + border-radius: 0.5rem 0.5rem 0 0; + overflow: clip; } .air-data { flex: 1 0; @@ -121,6 +162,9 @@ button.primary { .ground { position: relative; height: 3rem; + + border-radius: 0 0 0.5rem 0.5rem; + overflow: clip; } .ground-btn, @@ -176,7 +220,7 @@ button.primary { display: flex; flex-flow: row nowrap; align-items: stretch; - justify-content: flex-start; + justify-content: center; } .extra-data { width: 1rem; diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..4b6b3b9 Binary files /dev/null and b/public/icon.png differ diff --git a/public/index.html b/public/index.html index 5da7ed3..c05072b 100644 --- a/public/index.html +++ b/public/index.html @@ -1,18 +1,24 @@ - - - + + + - Svelte app + Svelte app - - - + + + - - + + - - + +
+
+ +
+
 slidershim
+
+ diff --git a/res/sshelper/index.html b/res/sshelper/index.html new file mode 100644 index 0000000..23b27ec --- /dev/null +++ b/res/sshelper/index.html @@ -0,0 +1,64 @@ + + + + + + + + brokenithm-qr + + +

slidershim brokenithm helper

+ + + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 45cf2d8..6caaae9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -225,6 +225,12 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +[[package]] +name = "bytemuck" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" + [[package]] name = "byteorder" version = "1.4.3" @@ -328,6 +334,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "checked_int_cast" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cc5e6b5ab06331c33589842070416baa137e8b0eb912b008cfd4a78ada7919" + [[package]] name = "cocoa" version = "0.24.0" @@ -359,6 +371,12 @@ dependencies = [ "objc", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "com" version = "0.2.0" @@ -1073,6 +1091,16 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] +[[package]] +name = "gif" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3a7187e78088aead22ceedeee99779455b23fc231fe13ec443f99bb71694e5b" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gio" version = "0.14.8" @@ -1433,6 +1461,25 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.23.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "gif", + "jpeg-decoder", + "num-iter", + "num-rational", + "num-traits", + "png 0.16.8", + "scoped_threadpool", + "tiff", +] + [[package]] name = "indexmap" version = "1.8.0" @@ -1537,6 +1584,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.56" @@ -1872,6 +1928,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -2357,6 +2424,16 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "qrcode" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d2f1455f3630c6e5107b4f2b94e74d76dea80736de0981fd27644216cff57f" +dependencies = [ + "checked_int_cast", + "image", +] + [[package]] name = "quote" version = "1.0.15" @@ -2649,6 +2726,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.1.0" @@ -2853,15 +2936,19 @@ name = "slidershim" version = "0.1.1" dependencies = [ "async-trait", + "base64", "directories", "env_logger", "futures", "futures-util", "hyper", + "image", "ipconfig", "log", + "open", "palette", "path-clean", + "qrcode", "rusb", "serde", "serde_json", @@ -3336,6 +3423,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +dependencies = [ + "jpeg-decoder", + "miniz_oxide 0.4.4", + "weezl", +] + [[package]] name = "time" version = "0.1.43" @@ -3805,6 +3903,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "weezl" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" + [[package]] name = "widestring" version = "0.5.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0231800..9a5b3b2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ serde = { version = "1.0", features = ["derive"] } log = "0.4.14" env_logger = "0.9.0" simple-logging = "2.0.2" +open = "2.0.2" tauri = { version = "1.0.0-beta.8", features = ["shell-open", "system-tray"] } futures = "0.3.19" @@ -37,6 +38,9 @@ winapi = "0.3.9" ipconfig = "0.3.0" hyper = { version="0.14.16", features= ["server", "http1", "http2", "tcp", "stream", "runtime"] } +base64 = "0.13.0" +image = "0.23.14" +qrcode = { version="0.12.0", features= ["image"] } path-clean = "0.1.0" tungstenite = { version="0.16.0", default-features=false } tokio-tungstenite = "0.16.1" diff --git a/src-tauri/res/www/app.js b/src-tauri/res/www/app.js index 59828f1..de70579 100644 --- a/src-tauri/res/www/app.js +++ b/src-tauri/res/www/app.js @@ -1 +1 @@ -var throttle=function(e,t){var a=!0,n=null;return function o(){var s=this;a?(a=!1,setTimeout(function(){a=!0,n&&o.apply(s)},t),n?(e.apply(this,n),n=null):e.apply(this,arguments)):n=arguments}},keys=document.getElementsByClassName("key"),airKeys=[],midline=0,touchKeys=[],allKeys=[],topKeys=airKeys,bottomKeys=touchKeys,compileKey=function(e){var t=e.previousElementSibling,a=e.nextElementSibling;return{top:e.offsetTop,bottom:e.offsetTop+e.offsetHeight,left:e.offsetLeft,right:e.offsetLeft+e.offsetWidth,almostLeft:t?e.offsetLeft+e.offsetWidth/4:-99999,almostRight:a?e.offsetLeft+3*e.offsetWidth/4:99999,kflag:parseInt(e.dataset.kflag)+(parseInt(e.dataset.air)?32:0),isAir:!!parseInt(e.dataset.air),prevKeyRef:t,prevKeyKflag:t?parseInt(t.dataset.kflag)+(parseInt(t.dataset.air)?32:0):null,nextKeyRef:a,nextKeyKflag:a?parseInt(a.dataset.kflag)+(parseInt(a.dataset.air)?32:0):null,ref:e}},isInside=function(e,t,a){return a.left<=e&&e2)return wsTimeout=0,ws.close(),wsConnected=!1,void wsConnect();wsConnected&&ws.send("alive?")},canvas=document.getElementById("canvas"),canvasCtx=canvas.getContext("2d"),canvasData=canvasCtx.getImageData(0,0,33,1),setupLed=function(){for(var e=0;e<33;e++)canvasData.data[4*e+3]=255};setupLed();var updateLed=function(e){for(var t=new Uint8Array(e),a=0;a<32;a++)canvasData.data[4*a]=t[3*(31-a)+1],canvasData.data[4*a+1]=t[3*(31-a)+2],canvasData.data[4*a+2]=t[3*(31-a)+0];canvasData.data[128]=t[94],canvasData.data[129]=t[95],canvasData.data[130]=t[93],canvasCtx.putImageData(canvasData,0,0)},fs=document.getElementById("fullscreen"),requestFullscreen=function(){!document.fullscreenElement&&screen.height<=1024&&(fs.requestFullscreen?fs.requestFullscreen():fs.mozRequestFullScreen?fs.mozRequestFullScreen():fs.webkitRequestFullScreen&&fs.webkitRequestFullScreen())},throttledRequestFullscreen=throttle(requestFullscreen,3e3),cnt=document.getElementById("main");cnt.addEventListener("touchstart",updateTouches),cnt.addEventListener("touchmove",updateTouches),cnt.addEventListener("touchend",updateTouches);var readConfig=function(e){var t="";e.invert&&(t+=".container, .air-container {flex-flow: column-reverse nowrap;} ");var a=e.bgColor||"rbga(0, 0, 0, 0.9)";e.bgImage?t+="#fullscreen {background: ".concat(a,' url("').concat(e.bgImage,'") fixed center / cover!important; background-repeat: no-repeat;} '):t+="#fullscreen {background: ".concat(a,";} "),"number"==typeof e.ledOpacity&&(0===e.ledOpacity?t+="#canvas {display: none} ":t+="#canvas {opacity: ".concat(e.ledOpacity,"} ")),"string"==typeof e.keyColor&&(t+=".key[data-active] {background-color: ".concat(e.keyColor,";} ")),"string"==typeof e.keyColor&&(t+=".key.air[data-active] {background-color: ".concat(e.lkeyColor,";} ")),"string"==typeof e.keyBorderColor&&(t+=".key {border: 1px solid ".concat(e.keyBorderColor,";} ")),e.keyColorFade&&"number"==typeof e.keyColorFade&&(t+=".key:not([data-active]) {transition: background ".concat(e.keyColorFade,"ms ease-out;} ")),"number"==typeof e.keyHeight&&(0===e.keyHeight?t+=".touch-container {display: none;} ":t+=".touch-container {flex: ".concat(e.keyHeight,";} ")),"number"==typeof e.lkeyHeight&&(0===e.lkeyHeight?t+=".air-container {display: none;} ":t+=".air-container {flex: ".concat(e.keyHeight,";} "));var n=document.createElement("style");n.innerHTML=t,document.head.appendChild(n)},initialize=function(){readConfig(config),compileKeys(),wsConnect(),setInterval(wsWatch,1e3)};initialize(),window.onresize=compileKeys; \ No newline at end of file +const throttle=(e,t)=>{var a=!0,s=null;return function n(){var o=this;a?(a=!1,setTimeout(function(){a=!0,s&&n.apply(o)},t),s?(e.apply(this,s),s=null):e.apply(this,arguments)):s=arguments}};var keys=document.getElementsByClassName("key"),airKeys=[],midline=0,touchKeys=[],allKeys=[],topKeys=airKeys,bottomKeys=touchKeys;const compileKey=e=>{const t=e.previousElementSibling,a=e.nextElementSibling;return{top:e.offsetTop,bottom:e.offsetTop+e.offsetHeight,left:e.offsetLeft,right:e.offsetLeft+e.offsetWidth,almostLeft:t?e.offsetLeft+e.offsetWidth/4:-99999,almostRight:a?e.offsetLeft+3*e.offsetWidth/4:99999,kflag:parseInt(e.dataset.kflag)+(parseInt(e.dataset.air)?32:0),isAir:!!parseInt(e.dataset.air),prevKeyRef:t,prevKeyKflag:t?parseInt(t.dataset.kflag)+(parseInt(t.dataset.air)?32:0):null,nextKeyRef:a,nextKeyKflag:a?parseInt(a.dataset.kflag)+(parseInt(a.dataset.air)?32:0):null,ref:e}},isInside=(e,t,a)=>a.left<=e&&e{keys=document.getElementsByClassName("key"),airKeys=[],touchKeys=[];for(var e=0;e{if(t{var s=t;e[s]&&!a&&s++,e[s]=1},sendKeys=e=>{wsConnected&&ws.send("b"+e.join(""))},throttledSendKeys=throttle(sendKeys,10);var ws=null,wsTimeout=0,wsConnected=!1;const wsConnect=()=>{(ws=new WebSocket("ws://"+location.host+"/ws")).binaryType="arraybuffer",ws.onopen=(()=>{ws.send("alive?")}),ws.onmessage=(e=>{e.data.byteLength?updateLed(e.data):"alive"==e.data&&(wsTimeout=0,wsConnected=!0)})},wsWatch=()=>{if(wsTimeout++>2)return wsTimeout=0,ws.close(),wsConnected=!1,void wsConnect();wsConnected&&ws.send("alive?")};var canvas=document.getElementById("canvas"),canvasCtx=canvas.getContext("2d"),canvasData=canvasCtx.getImageData(0,0,33,1);const setupLed=()=>{for(var e=0;e<33;e++)canvasData.data[4*e+3]=255};setupLed();const updateLed=e=>{const t=new Uint8Array(e);for(var a=0;a<31;a++)canvasData.data[4*a+4]=t[3*a],canvasData.data[4*a+5]=t[3*a+1],canvasData.data[4*a+6]=t[3*a+2];canvasData.data[0]=t[0],canvasData.data[1]=t[1],canvasData.data[2]=t[2],canvasData.data[128]=t[90],canvasData.data[129]=t[91],canvasData.data[130]=t[92],canvasCtx.putImageData(canvasData,0,0)},fs=document.getElementById("fullscreen"),requestFullscreen=()=>{!document.fullscreenElement&&screen.height<=1024&&(fs.requestFullscreen?fs.requestFullscreen():fs.mozRequestFullScreen?fs.mozRequestFullScreen():fs.webkitRequestFullScreen&&fs.webkitRequestFullScreen())},throttledRequestFullscreen=throttle(requestFullscreen,3e3),cnt=document.getElementById("main");cnt.addEventListener("touchstart",updateTouches),cnt.addEventListener("touchmove",updateTouches),cnt.addEventListener("touchend",updateTouches);const readConfig=e=>{var t="";e.invert&&(t+=".container, .air-container {flex-flow: column-reverse nowrap;} ");var a=e.bgColor||"rbga(0, 0, 0, 0.9)";e.bgImage?t+=`#fullscreen {background: ${a} url("${e.bgImage}") fixed center / cover!important; background-repeat: no-repeat;} `:t+=`#fullscreen {background: ${a};} `,"number"==typeof e.ledOpacity&&(0===e.ledOpacity?t+="#canvas {display: none} ":t+=`#canvas {opacity: ${e.ledOpacity}} `),"string"==typeof e.keyColor&&(t+=`.key[data-active] {background-color: ${e.keyColor};} `),"string"==typeof e.keyColor&&(t+=`.key.air[data-active] {background-color: ${e.lkeyColor};} `),"string"==typeof e.keyBorderColor&&(t+=`.key {border: 1px solid ${e.keyBorderColor};} `),e.keyColorFade&&"number"==typeof e.keyColorFade&&(t+=`.key:not([data-active]) {transition: background ${e.keyColorFade}ms ease-out;} `),"number"==typeof e.keyHeight&&(0===e.keyHeight?t+=".touch-container {display: none;} ":t+=`.touch-container {flex: ${e.keyHeight};} `),"number"==typeof e.lkeyHeight&&(0===e.lkeyHeight?t+=".air-container {display: none;} ":t+=`.air-container {flex: ${e.keyHeight};} `);var s=document.createElement("style");s.innerHTML=t,document.head.appendChild(s)},initialize=()=>{readConfig(config),compileKeys(),wsConnect(),setInterval(wsWatch,1e3)};readConfig(config),compileKeys(),wsConnect(),setInterval(wsWatch,1e3),window.onresize=compileKeys; \ No newline at end of file diff --git a/src-tauri/res/www/index-go.html b/src-tauri/res/www/index-go.html index 67d85f4..8a3db37 100644 --- a/src-tauri/res/www/index-go.html +++ b/src-tauri/res/www/index-go.html @@ -67,6 +67,9 @@ } canvas { + -ms-interpolation-mode: nearest-neighbor; + image-rendering: crisp-edges; + image-rendering: pixelated; touch-action: none; margin: 0px -1.5625vw; } @@ -112,6 +115,6 @@ - + diff --git a/src-tauri/res/www/index.html b/src-tauri/res/www/index.html index b4dd603..f0cc9e4 100644 --- a/src-tauri/res/www/index.html +++ b/src-tauri/res/www/index.html @@ -66,6 +66,9 @@ } canvas { + -ms-interpolation-mode: nearest-neighbor; + image-rendering: crisp-edges; + image-rendering: pixelated; touch-action: none; margin: 0px -1.5625vw; } @@ -111,6 +114,6 @@ - + diff --git a/src-tauri/res/www/src.js b/src-tauri/res/www/src.js index 94cd9e4..3e0d5e8 100644 --- a/src-tauri/res/www/src.js +++ b/src-tauri/res/www/src.js @@ -236,15 +236,18 @@ const setupLed = () => { setupLed(); const updateLed = (data) => { const buf = new Uint8Array(data); - for (var i = 0; i < 32; i++) { - canvasData.data[i * 4] = buf[(31 - i) * 3 + 1]; // r - canvasData.data[i * 4 + 1] = buf[(31 - i) * 3 + 2]; // g - canvasData.data[i * 4 + 2] = buf[(31 - i) * 3 + 0]; // b + for (var i = 0; i < 31; i++) { + canvasData.data[i * 4 + 4] = buf[i * 3]; // r + canvasData.data[i * 4 + 5] = buf[i * 3 + 1]; // g + canvasData.data[i * 4 + 6] = buf[i * 3 + 2]; // b } - // Copy from first led - canvasData.data[128] = buf[94]; - canvasData.data[129] = buf[95]; - canvasData.data[130] = buf[93]; + canvasData.data[0] = buf[0] + canvasData.data[1] = buf[1] + canvasData.data[2] = buf[2] + canvasData.data[128] = buf[90]; + canvasData.data[129] = buf[91]; + canvasData.data[130] = buf[92]; + canvasCtx.putImageData(canvasData, 0, 0); }; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fe1f2b5..4523094 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -9,7 +9,6 @@ mod slider_io; use std::sync::{Arc, Mutex}; -// use env_logger; use log::info; use tauri::{ @@ -32,9 +31,18 @@ fn quit_app() { fn main() { // Setup logger - let log_file_path = slider_io::Config::get_log_file_path().unwrap(); - simple_logging::log_to_file(log_file_path.as_path(), log::LevelFilter::Debug).unwrap(); - // simple_logging::log_to_file("./log.txt", log::LevelFilter::Debug).unwrap(); + + #[cfg(debug_assertions)] + env_logger::Builder::new() + .filter_level(log::LevelFilter::Debug) + .init(); + + #[cfg(not(debug_assertions))] + { + let log_file_path = slider_io::Config::get_log_file_path().unwrap(); + simple_logging::log_to_file(log_file_path.as_path(), log::LevelFilter::Debug).unwrap(); + // simple_logging::log_to_file("./log.txt", log::LevelFilter::Debug).unwrap(); + } let config = Arc::new(Mutex::new(Some(slider_io::Config::default()))); let manager = Arc::new(Mutex::new(slider_io::Manager::new())); @@ -92,6 +100,22 @@ fn main() { quit_app(); }); + // Show logs + app.listen_global("openLogfile", |_| { + let log_file_path = slider_io::Config::get_log_file_path(); + if let Some(log_file_path) = log_file_path { + open::that(log_file_path.as_path()).ok(); + } + }); + + // Show brokenithm qr + app.listen_global("openBrokenithmQr", |_| { + let brokenithm_qr_path = slider_io::Config::get_brokenithm_qr_path(); + if let Some(brokenithm_qr_path) = brokenithm_qr_path { + open::that(brokenithm_qr_path.as_path()).ok(); + } + }); + // UI ready event let app_handle = app.handle(); let config_clone = Arc::clone(&config); @@ -109,11 +133,6 @@ fn main() { if let Ok(ips) = ips { app_handle.emit_all("listIps", &ips).unwrap(); } - - let log_file_path = slider_io::Config::get_log_file_path().unwrap(); - app_handle - .emit_all("updateLogPath", log_file_path.as_path().to_str().unwrap()) - .unwrap(); }); // UI update event diff --git a/src-tauri/src/slider_io/brokenithm.rs b/src-tauri/src/slider_io/brokenithm.rs index a3c13c2..81316b6 100644 --- a/src-tauri/src/slider_io/brokenithm.rs +++ b/src-tauri/src/slider_io/brokenithm.rs @@ -10,7 +10,12 @@ use hyper::{ use log::{error, info}; use path_clean::PathClean; use std::{convert::Infallible, env::current_exe, future::Future, net::SocketAddr}; -use tokio::fs::File; +use tokio::{ + fs::File, + select, + sync::mpsc, + time::{sleep, Duration}, +}; use tokio_tungstenite::WebSocketStream; use tokio_util::codec::{BytesCodec, FramedRead}; use tungstenite::{handshake, Message}; @@ -51,67 +56,111 @@ async fn serve_file(path: &str) -> Result, Infallible> { async fn handle_brokenithm(ws_stream: WebSocketStream, state: FullState) { let (mut ws_write, mut ws_read) = ws_stream.split(); - loop { - match ws_read.next().await { - Some(msg) => match msg { - Ok(msg) => match msg { - Message::Text(msg) => { - let mut chars = msg.chars(); - let head = chars.next().unwrap(); - match head { - 'a' => { - ws_write - .send(Message::Text("alive".to_string())) - .await - .unwrap(); - } - 'b' => { - let flat_state: Vec = chars - .map(|x| match x { - '0' => false, - '1' => true, - _ => unreachable!(), - }) - .collect(); - let mut controller_state_handle = state.controller_state.lock().unwrap(); - for (idx, c) in flat_state[0..32].iter().enumerate() { - controller_state_handle.ground_state[idx] = match c { - false => 0, - true => 255, - } - } - for (idx, c) in flat_state[32..38].iter().enumerate() { - controller_state_handle.air_state[idx] = match c { - false => 0, - true => 1, - } - } - // println!( - // "{:?} {:?}", - // controller_state_handle.ground_state, controller_state_handle.air_state - // ); - } - _ => { - break; - } - } - } - Message::Close(_) => { - info!("Websocket connection closed"); + let (msg_write, mut msg_read) = mpsc::unbounded_channel::(); + + let write_task = async move { + // info!("Websocket write task open"); + loop { + match msg_read.recv().await { + Some(msg) => match ws_write.send(msg).await.ok() { + Some(_) => {} + None => { break; } - _ => {} }, - Err(e) => { - error!("Websocket connection error: {}", e); + None => { break; } - }, - None => { - break; } } - } + // info!("Websocket write task done"); + }; + + let msg_write_handle = msg_write.clone(); + let state_handle = state.clone(); + let read_task = async move { + // info!("Websocket read task open"); + loop { + match ws_read.next().await { + Some(msg) => match msg { + Ok(msg) => match msg { + Message::Text(msg) => { + let mut chars = msg.chars(); + let head = chars.next().unwrap(); + match head { + 'a' => { + msg_write_handle + .send(Message::Text("alive".to_string())) + .ok(); + } + 'b' => { + let flat_state: Vec = chars + .map(|x| match x { + '0' => false, + '1' => true, + _ => unreachable!(), + }) + .collect(); + let mut controller_state_handle = state_handle.controller_state.lock().unwrap(); + for (idx, c) in flat_state[0..32].iter().enumerate() { + controller_state_handle.ground_state[idx] = match c { + false => 0, + true => 255, + } + } + for (idx, c) in flat_state[32..38].iter().enumerate() { + controller_state_handle.air_state[idx] = match c { + false => 0, + true => 1, + } + } + } + _ => { + break; + } + } + } + Message::Close(_) => { + info!("Websocket connection closed"); + break; + } + _ => {} + }, + Err(e) => { + error!("Websocket connection error: {}", e); + break; + } + }, + None => { + break; + } + } + } + // info!("Websocket read task done"); + }; + + let msg_write_handle = msg_write.clone(); + let state_handle = state.clone(); + let led_task = async move { + loop { + let mut led_data = vec![0; 93]; + { + let led_state_handle = state_handle.led_state.lock().unwrap(); + (&mut led_data).copy_from_slice(&led_state_handle.led_state); + } + msg_write_handle.send(Message::Binary(led_data)).ok(); + + sleep(Duration::from_millis(50)).await; + } + }; + + info!("Websocket handling"); + select! { + _ = read_task => {} + _ = write_task => {} + _ = led_task => {} + }; + info!("Websocket done"); } async fn handle_websocket( diff --git a/src-tauri/src/slider_io/config.rs b/src-tauri/src/slider_io/config.rs index b6b8daa..3299768 100644 --- a/src-tauri/src/slider_io/config.rs +++ b/src-tauri/src/slider_io/config.rs @@ -1,8 +1,12 @@ use directories::ProjectDirs; +use image::Luma; use log::{info, warn}; +use qrcode::QrCode; use serde_json::Value; use std::{convert::TryFrom, fs, path::PathBuf}; +use crate::slider_io::utils::list_ips; + #[derive(Debug, Clone)] pub enum DeviceMode { None, @@ -12,6 +16,38 @@ pub enum DeviceMode { Brokenithm { ground_only: bool }, } +#[derive(Debug, Clone, Copy)] +pub enum OutputPolling { + Sixty, + Hundred, + ThreeHundred, + FiveHundred, + Thousand, +} + +impl OutputPolling { + pub fn from_str(s: &str) -> Option { + match s { + "60" => Some(OutputPolling::Sixty), + "100" => Some(OutputPolling::Hundred), + "330" => Some(OutputPolling::ThreeHundred), + "500" => Some(OutputPolling::FiveHundred), + "1000" => Some(OutputPolling::Thousand), + _ => None, + } + } + + pub fn to_t_u64(&self) -> u64 { + match self { + OutputPolling::Sixty => 16, + OutputPolling::Hundred => 10, + OutputPolling::ThreeHundred => 3, + OutputPolling::FiveHundred => 2, + OutputPolling::Thousand => 1, + } + } +} + #[derive(Debug, Clone, Copy)] pub enum KeyboardLayout { Tasoller, @@ -31,14 +67,17 @@ pub enum OutputMode { None, Keyboard { layout: KeyboardLayout, + polling: OutputPolling, sensitivity: u8, }, Gamepad { layout: GamepadLayout, + polling: OutputPolling, sensitivity: u8, }, Websocket { url: String, + polling: OutputPolling, }, } @@ -92,30 +131,37 @@ impl Config { "none" => OutputMode::None, "kb-32-tasoller" => OutputMode::Keyboard { layout: KeyboardLayout::Tasoller, + polling: OutputPolling::from_str(v["outputPolling"].as_str()?)?, sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?, }, "kb-32-yuancon" => OutputMode::Keyboard { layout: KeyboardLayout::Yuancon, + polling: OutputPolling::from_str(v["outputPolling"].as_str()?)?, sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?, }, "kb-8-deemo" => OutputMode::Keyboard { layout: KeyboardLayout::Deemo, + polling: OutputPolling::from_str(v["outputPolling"].as_str()?)?, sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?, }, "kb-voltex" => OutputMode::Keyboard { layout: KeyboardLayout::Voltex, + polling: OutputPolling::from_str(v["outputPolling"].as_str()?)?, sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?, }, "gamepad-voltex" => OutputMode::Gamepad { layout: GamepadLayout::Voltex, + polling: OutputPolling::from_str(v["outputPolling"].as_str()?)?, sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?, }, "gamepad-neardayo" => OutputMode::Gamepad { layout: GamepadLayout::Neardayo, + polling: OutputPolling::from_str(v["outputPolling"].as_str()?)?, sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?, }, "websocket" => OutputMode::Websocket { url: v["outputWebsocketUrl"].as_str()?.to_string(), + polling: OutputPolling::from_str(v["outputPolling"].as_str()?)?, }, _ => panic!("Invalid output mode"), }, @@ -158,6 +204,7 @@ impl Config { "ledMode": "none", "keyboardSensitivity": 20, "outputWebsocketUrl": "localhost:3000", + "outputPolling": "60", "ledSensitivity": 20, "ledWebsocketUrl": "localhost:3001", "ledSerialPort": "COM5" @@ -176,6 +223,28 @@ impl Config { return Some(Box::new(log_path)); } + pub fn get_brokenithm_qr_path() -> Option> { + let project_dir = ProjectDirs::from("me", "impress labs", "slidershim").unwrap(); + let config_dir = project_dir.config_dir(); + fs::create_dir_all(config_dir).unwrap(); + + let brokenithm_qr_path = config_dir.join("brokenithm.png"); + + let ips = list_ips().ok()?; + let link = "http://imp.ress.me/t/sshelper?d=".to_string() + + &ips + .into_iter() + .filter(|s| s.as_str().chars().filter(|x| *x == '.').count() == 3) + .map(|s| base64::encode_config(s, base64::URL_SAFE_NO_PAD)) + .collect::>() + .join(";"); + let qr = QrCode::new(link).ok()?; + let image = qr.render::>().build(); + image.save(brokenithm_qr_path.as_path()).ok()?; + + return Some(Box::new(brokenithm_qr_path)); + } + fn get_saved_path() -> Option> { let project_dir = ProjectDirs::from("me", "impress labs", "slidershim").unwrap(); let config_dir = project_dir.config_dir(); diff --git a/src-tauri/src/slider_io/device.rs b/src-tauri/src/slider_io/device.rs index 84a189b..0706456 100644 --- a/src-tauri/src/slider_io/device.rs +++ b/src-tauri/src/slider_io/device.rs @@ -102,8 +102,8 @@ impl HidDeviceJob { .zip(led_state.led_state.chunks(3).rev()) { buf_chunk[0] = state_chunk[2]; - buf_chunk[1] = state_chunk[1]; - buf_chunk[2] = state_chunk[0]; + buf_chunk[1] = state_chunk[0]; + buf_chunk[2] = state_chunk[1]; } buf.data[96..240].fill(0); }, @@ -140,8 +140,8 @@ impl HidDeviceJob { .zip(led_state.led_state.chunks(3).rev()) { buf_chunk[0] = state_chunk[2]; - buf_chunk[1] = state_chunk[1]; - buf_chunk[2] = state_chunk[0]; + buf_chunk[1] = state_chunk[0]; + buf_chunk[2] = state_chunk[1]; } buf.data[96..240].fill(0); }, diff --git a/src-tauri/src/slider_io/led.rs b/src-tauri/src/slider_io/led.rs index 7bad71e..c70b6cd 100644 --- a/src-tauri/src/slider_io/led.rs +++ b/src-tauri/src/slider_io/led.rs @@ -137,7 +137,7 @@ impl LedJob { { led_state.paint(idx, &[(*buf_chunk)[1], (*buf_chunk)[2], (*buf_chunk)[0]]); } - println!("leds {:?}", led_state.led_state); + // println!("leds {:?}", led_state.led_state); } } } @@ -187,23 +187,21 @@ impl ThreadJob for LedJob { LedMode::Serial { .. } => { if let Some(serial_port) = self.serial_port.as_mut() { let mut serial_data_avail = serial_port.bytes_to_read().unwrap_or(0); - if serial_data_avail < 100 { - return; - } + if serial_data_avail >= 100 { + if serial_data_avail % 100 == 0 { + let mut serial_buffer_working = Buffer::new(); + serial_port + .as_mut() + .read_exact(&mut serial_buffer_working.data[..100]) + .ok() + .unwrap(); + serial_data_avail -= 100; + serial_buffer = Some(serial_buffer_working); + } - if serial_data_avail % 100 == 0 { - let mut serial_buffer_working = Buffer::new(); - serial_port - .as_mut() - .read_exact(&mut serial_buffer_working.data[..100]) - .ok() - .unwrap(); - serial_data_avail -= 100; - serial_buffer = Some(serial_buffer_working); - } - - if serial_data_avail > 0 { - serial_port.clear(ClearBuffer::All).unwrap(); + if serial_data_avail > 0 { + serial_port.clear(ClearBuffer::All).unwrap(); + } } } } diff --git a/src-tauri/src/slider_io/manager.rs b/src-tauri/src/slider_io/manager.rs index ab3f34c..1266c11 100644 --- a/src-tauri/src/slider_io/manager.rs +++ b/src-tauri/src/slider_io/manager.rs @@ -33,7 +33,7 @@ impl Manager { let join_handle = thread::spawn(move || { info!("Manager thread started"); let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(1) + .worker_threads(2) .enable_all() .build() .unwrap(); diff --git a/src-tauri/src/slider_io/output.rs b/src-tauri/src/slider_io/output.rs index ff65c1d..8f13f30 100644 --- a/src-tauri/src/slider_io/output.rs +++ b/src-tauri/src/slider_io/output.rs @@ -12,6 +12,7 @@ pub trait OutputHandler: Send { pub struct OutputJob { state: FullState, + t: u64, sensitivity: u8, handler: Box, } @@ -21,17 +22,21 @@ impl OutputJob { match mode { OutputMode::Keyboard { layout, + polling, sensitivity, } => Self { state: state.clone(), + t: polling.to_t_u64(), sensitivity: *sensitivity, handler: Box::new(KeyboardOutput::new(layout.clone())), }, OutputMode::Gamepad { layout, + polling, sensitivity, } => Self { state: state.clone(), + t: polling.to_t_u64(), sensitivity: *sensitivity, handler: Box::new(GamepadOutput::new(layout.clone())), }, @@ -53,7 +58,7 @@ impl ThreadJob for OutputJob { } self.handler.tick(&flat_controller_state); - thread::sleep(Duration::from_millis(10)); + thread::sleep(Duration::from_millis(self.t)); } fn teardown(&mut self) { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1d0f458..773d8ff 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -51,10 +51,7 @@ "active": false }, "allowlist": { - "all": false, - "shell": { - "open": true - } + "all": false }, "windows": [ { @@ -62,7 +59,9 @@ "width": 500, "height": 550, "resizable": false, - "fullscreen": false + "fullscreen": false, + "decorations": false, + "transparent": true } ], "security": { diff --git a/src/App.svelte b/src/App.svelte index 50f2bac..23bfb29 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -11,6 +11,7 @@ let ledMode = "none"; let keyboardSensitivity = 20; + let outputPolling = "100"; let outputWebsocketUrl = "http://localhost:3000"; let ledSensitivity = 20; let ledWebsocketUrl = "http://localhost:3001"; @@ -28,7 +29,6 @@ let polling = null; let tick = 0; let previewData = Array(131).fill(0); - let logfile = ""; function updatePolling(enabled) { if (!!polling) { @@ -44,6 +44,7 @@ // console.log(enabled, polling, tick); } + // Receive events onMount(async () => { // console.log(emit, listen); await listen("showConfig", (event) => { @@ -51,7 +52,9 @@ deviceMode = payload.deviceMode || "none"; outputMode = payload.outputMode || "none"; ledMode = payload.ledMode || "none"; + keyboardSensitivity = payload.keyboardSensitivity || 20; + outputPolling = payload.outputPolling || "100"; outputWebsocketUrl = payload.outputWebsocketUrl || "http://localhost:3000/"; ledSensitivity = payload.ledSensitivity || 20; @@ -69,10 +72,6 @@ ); }); - await listen("updateLogPath", (event) => { - logfile = event.payload as string; - }); - await emit("ready", ""); updatePolling(true); @@ -86,6 +85,8 @@ }); }); + // Emit events + async function setConfig() { console.log("Updating config"); await emit( @@ -95,6 +96,7 @@ outputMode, ledMode, keyboardSensitivity, + outputPolling, outputWebsocketUrl, ledSensitivity, ledWebsocketUrl, @@ -114,17 +116,21 @@ } async function logs() { - await open(logfile); + await emit("openLogfile", ""); + } + + async function brokenithmQr() { + await emit("openBrokenithmQr"); }
-
-
- - slidershim -
-
+ + + + + + @@ -149,7 +155,7 @@
- Brokenithm open at: + Brokenithm server running, access at one of:
             {ips.map((x) => `http://${x}:1606/`).join("\n")}
           
@@ -175,7 +181,21 @@
- {#if outputMode === "gamepad-voltex"} + {#if outputMode !== "none"} +
+
Output Polling
+
+ +
+
+ {/if} + {#if outputMode.slice(0, 7) === "gamepad"}
@@ -187,7 +207,9 @@ {/if} {#if outputMode.slice(0, 2) === "kb" && deviceMode.slice(0, 10) !== "brokenithm"}
-
Sensitivity
+
+ Sensitivity +
{#if ledMode.slice(0, 8) === "reactive" && deviceMode.slice(0, 10) !== "brokenithm"}
-
Sensitivity
+
+ Sensitivity +
- {#if !!logfile.length} - + + {#if deviceMode.slice(0, 10) === "brokenithm"} + {/if}
diff --git a/src/Preview.svelte b/src/Preview.svelte index a355472..758f4e2 100644 --- a/src/Preview.svelte +++ b/src/Preview.svelte @@ -5,12 +5,9 @@ let botDatas = Array(16).fill(0); let airDatas = Array(6).fill(0); let extraDatas = Array(3).fill(0); - let ledDatas = Array(31) - .fill(0) - .map((_, idx) => ({ - color: !!(idx % 2) ? "#f0f" : "#ff0", - spec: idx % 2, - })); + + let ledDatas = Array(16).fill("#ff0"); + let ledDividerDatas = Array(15).fill("#ff0"); $: { if (data.length === 134) { @@ -27,9 +24,14 @@ } for (let i = 0; i < 31; i++) { - ledDatas[i].color = `rgb(${data[41 + i * 3]}, ${data[42 + i * 3]}, ${ + let rgbstr = `rgb(${data[41 + i * 3]}, ${data[42 + i * 3]}, ${ data[43 + i * 3] })`; + if (i % 2 == 0) { + ledDatas[i / 2] = rgbstr; + } else { + ledDividerDatas[(i - 1) / 2] = rgbstr; + } } } } @@ -44,11 +46,18 @@
+ {#each ledDatas as ledData, idx (idx)} +
+ {/each} +
+
+
+
- {#each ledDatas as { color, spec }, idx (idx)} + {#each ledDividerDatas as ledDividerData, idx (idx)}
{/each}