diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 281bbaf..b0c38c9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2247,6 +2247,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "path-clean" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecba01bf2678719532c5e3059e0b5f0811273d94b397088b82e3bd0a78c78fdd" + [[package]] name = "pathdiff" version = "0.2.1" @@ -3056,9 +3062,11 @@ dependencies = [ "directories", "env_logger", "futures", + "futures-util", "hyper", "log", "palette", + "path-clean", "rusb", "serde", "serde_json", @@ -3067,6 +3075,7 @@ dependencies = [ "tauri-build", "tokio", "tokio-tungstenite", + "tokio-util", "tungstenite", "vigem-client", "winapi", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5100d3e..31bf82d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,6 +22,12 @@ log = "0.4.14" env_logger = "0.9.0" tauri = { version = "1.0.0-beta.8", features = ["api-all", "system-tray"] } +futures = "0.3.19" +futures-util = "0.3.19" +async-trait = "0.1.52" +tokio = { version="1.16.1", features=["rt-multi-thread","macros"] } +tokio-util = "0.6.9" + directories = "4.0.1" rusb = "0.9.0" serialport = "4.0.1" @@ -29,10 +35,8 @@ vigem-client = "0.1.1" palette = "0.6.0" winapi = "0.3.9" -futures = "0.3.19" -async-trait = "0.1.52" -tokio = { version="1.16.1", features=["rt-multi-thread","macros"] } -hyper = { version="0.14.16", features=["server","http1","http2","tcp"] } +hyper = { version="0.14.16", features=["server", "http1", "http2", "tcp", "stream", "runtime"] } +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 new file mode 100644 index 0000000..59828f1 --- /dev/null +++ b/src-tauri/res/www/app.js @@ -0,0 +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 diff --git a/src-tauri/res/www/config.js b/src-tauri/res/www/config.js new file mode 100644 index 0000000..56114fa --- /dev/null +++ b/src-tauri/res/www/config.js @@ -0,0 +1,21 @@ +var config = { + // Inverted layout mode. + // Set to "true" to have the "lift" key at the bottom of the screen rather than the top. + invert: false, + + // Use a solid background color + bgImage: false, + bgColor: "#000000", + + // Use a custom background image + // bgColor is overlayed on the image, so make it semi-transparent + // bgImage: + // "https://raw.githubusercontent.com/gist/4yn/df052666266ce25554110ca1b4f33ce3/raw/1e5e6ab966639bb6786a4690dc49097763b16ba0/subtle-prism.svg", + // bgColor: "rgba(0, 0, 0, 0.5)", + + // Key Press Color + keyColor: "#FF00FF", + + // Lift key color + lkeyColor: "#00FFFF", +}; diff --git a/src-tauri/res/www/favicon.ico b/src-tauri/res/www/favicon.ico new file mode 100644 index 0000000..9ed6dc6 Binary files /dev/null and b/src-tauri/res/www/favicon.ico differ diff --git a/src-tauri/res/www/index.html b/src-tauri/res/www/index.html new file mode 100644 index 0000000..8728f08 --- /dev/null +++ b/src-tauri/res/www/index.html @@ -0,0 +1,116 @@ + + + + brokenithm-kb + + + + + + + +
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + diff --git a/src-tauri/res/www/src.js b/src-tauri/res/www/src.js new file mode 100644 index 0000000..94cd9e4 --- /dev/null +++ b/src-tauri/res/www/src.js @@ -0,0 +1,344 @@ +/* + Post-process with https://babeljs.io/repl and https://javascript-minifier.com/ +*/ + +const throttle = (func, wait) => { + var ready = true; + var args = null; + return function throttled() { + var context = this; + if (ready) { + ready = false; + setTimeout(function () { + ready = true; + if (args) { + throttled.apply(context); + } + }, wait); + if (args) { + func.apply(this, args); + args = null; + } else { + func.apply(this, arguments); + } + } else { + args = arguments; + } + }; +}; + +// Element refs +var keys = document.getElementsByClassName("key"); +var airKeys = []; +var midline = 0; +var touchKeys = []; +var allKeys = []; +var topKeys = airKeys; +var bottomKeys = touchKeys; +const compileKey = (key) => { + const prev = key.previousElementSibling; + const next = key.nextElementSibling; + return { + top: key.offsetTop, + bottom: key.offsetTop + key.offsetHeight, + left: key.offsetLeft, + right: key.offsetLeft + key.offsetWidth, + almostLeft: !!prev ? key.offsetLeft + key.offsetWidth / 4 : -99999, + almostRight: !!next ? key.offsetLeft + (key.offsetWidth * 3) / 4 : 99999, + kflag: parseInt(key.dataset.kflag) + (parseInt(key.dataset.air) ? 32 : 0), + isAir: parseInt(key.dataset.air) ? true : false, + prevKeyRef: prev, + prevKeyKflag: prev + ? parseInt(prev.dataset.kflag) + (parseInt(prev.dataset.air) ? 32 : 0) + : null, + nextKeyRef: next, + nextKeyKflag: next + ? parseInt(next.dataset.kflag) + (parseInt(next.dataset.air) ? 32 : 0) + : null, + ref: key, + }; +}; +const isInside = (x, y, compiledKey) => { + return ( + compiledKey.left <= x && + x < compiledKey.right && + compiledKey.top <= y && + y < compiledKey.bottom + ); +}; +const compileKeys = () => { + keys = document.getElementsByClassName("key"); + airKeys = []; + touchKeys = []; + for (var i = 0, key; i < keys.length; i++) { + const compiledKey = compileKey(keys[i]); + if (!compiledKey.isAir) { + touchKeys.push(compiledKey); + } else { + airKeys.push(compiledKey); + } + allKeys.push(compiledKey); + } + + if (!config.invert) { + // Not inverted + topKeys = airKeys; + bottomKeys = touchKeys; + midline = touchKeys[0].top; + } else { + // Inverted + topKeys = touchKeys; + bottomKeys = airKeys; + midline = touchKeys[0].bottom; + } +}; + +const getKey = (x, y) => { + if (y < midline) { + for (var i = 0; i < topKeys.length; i++) { + if (isInside(x, y, topKeys[i])) return topKeys[i]; + } + } else { + for (var i = 0; i < bottomKeys.length; i++) { + if (isInside(x, y, bottomKeys[i])) { + return bottomKeys[i]; + } + } + } + return null; +}; + +// Button State +// prettier-ignore +var lastState = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, +]; + +function updateTouches(e) { + try { + e.preventDefault(); + + // prettier-ignore + var keyFlags = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ]; + + throttledRequestFullscreen(); + + for (var i = 0; i < e.touches.length; i++) { + const touch = e.touches[i]; + + const x = touch.clientX; + const y = touch.clientY; + + const key = getKey(x, y); + + if (!key) continue; + + setKey(keyFlags, key.kflag, key.isAir); + + if (key.isAir) continue; + + if (x < key.almostLeft) { + setKey(keyFlags, key.prevKeyKflag, false); + } + + if (key.almostRight < x) { + setKey(keyFlags, key.nextKeyKflag, false); + } + } + + // Render keys + for (var i = 0; i < allKeys.length; i++) { + const key = allKeys[i]; + const kflag = key.kflag; + if (keyFlags[kflag] !== lastState[kflag]) { + if (keyFlags[kflag]) { + key.ref.setAttribute("data-active", ""); + } else { + key.ref.removeAttribute("data-active"); + } + } + } + + if (keyFlags !== lastState) { + throttledSendKeys(keyFlags); + } + lastState = keyFlags; + } catch (err) { + alert(err); + } +} +const throttledUpdateTouches = throttle(updateTouches, 10); + +const setKey = (keyFlags, kflag, isAir) => { + var idx = kflag; + if (keyFlags[idx] && !isAir) { + idx++; + } + keyFlags[idx] = 1; +}; + +const sendKeys = (keyFlags) => { + if (wsConnected) { + ws.send("b" + keyFlags.join("")); + } +}; +const throttledSendKeys = throttle(sendKeys, 10); + +// Websockets +var ws = null; +var wsTimeout = 0; +var wsConnected = false; +const wsConnect = () => { + ws = new WebSocket("ws://" + location.host + "/ws"); + ws.binaryType = "arraybuffer"; + ws.onopen = () => { + ws.send("alive?"); + }; + ws.onmessage = (e) => { + if (e.data.byteLength) { + updateLed(e.data); + } else if (e.data == "alive") { + wsTimeout = 0; + wsConnected = true; + } + }; +}; +const wsWatch = () => { + if (wsTimeout++ > 2) { + wsTimeout = 0; + ws.close(); + wsConnected = false; + wsConnect(); + return; + } + if (wsConnected) { + ws.send("alive?"); + } +}; + +// Canvas vars +var canvas = document.getElementById("canvas"); +var canvasCtx = canvas.getContext("2d"); +var canvasData = canvasCtx.getImageData(0, 0, 33, 1); +const setupLed = () => { + for (var i = 0; i < 33; i++) { + canvasData.data[i * 4 + 3] = 255; + } +}; +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 + } + // Copy from first led + canvasData.data[128] = buf[94]; + canvasData.data[129] = buf[95]; + canvasData.data[130] = buf[93]; + canvasCtx.putImageData(canvasData, 0, 0); +}; + +// Fullscreener +const fs = document.getElementById("fullscreen"); +const requestFullscreen = () => { + if (!document.fullscreenElement && screen.height <= 1024) { + if (fs.requestFullscreen) { + fs.requestFullscreen(); + } else if (fs.mozRequestFullScreen) { + fs.mozRequestFullScreen(); + } else if (fs.webkitRequestFullScreen) { + fs.webkitRequestFullScreen(); + } + } +}; +const throttledRequestFullscreen = throttle(requestFullscreen, 3000); + +// Do update hooks +const cnt = document.getElementById("main"); + +cnt.addEventListener("touchstart", updateTouches); +cnt.addEventListener("touchmove", updateTouches); +cnt.addEventListener("touchend", updateTouches); + +// cnt.addEventListener("touchstart", throttledUpdateTouches); +// cnt.addEventListener("touchmove", throttledUpdateTouches); +// cnt.addEventListener("touchend", throttledUpdateTouches); + +// Load config +const readConfig = (config) => { + var style = ""; + + if (!!config.invert) { + style += `.container, .air-container {flex-flow: column-reverse nowrap;} `; + } + + var bgColor = config.bgColor || "rbga(0, 0, 0, 0.9)"; + if (!config.bgImage) { + style += `#fullscreen {background: ${bgColor};} `; + } else { + style += `#fullscreen {background: ${bgColor} url("${config.bgImage}") fixed center / cover!important; background-repeat: no-repeat;} `; + } + + if (typeof config.ledOpacity === "number") { + if (config.ledOpacity === 0) { + style += `#canvas {display: none} `; + } else { + style += `#canvas {opacity: ${config.ledOpacity}} `; + } + } + + if (typeof config.keyColor === "string") { + style += `.key[data-active] {background-color: ${config.keyColor};} `; + } + if (typeof config.keyColor === "string") { + style += `.key.air[data-active] {background-color: ${config.lkeyColor};} `; + } + if (typeof config.keyBorderColor === "string") { + style += `.key {border: 1px solid ${config.keyBorderColor};} `; + } + if (!!config.keyColorFade && typeof config.keyColorFade === "number") { + style += `.key:not([data-active]) {transition: background ${config.keyColorFade}ms ease-out;} `; + } + + if (typeof config.keyHeight === "number") { + if (config.keyHeight === 0) { + style += `.touch-container {display: none;} `; + } else { + style += `.touch-container {flex: ${config.keyHeight};} `; + } + } + + if (typeof config.lkeyHeight === "number") { + if (config.lkeyHeight === 0) { + style += `.air-container {display: none;} `; + } else { + style += `.air-container {flex: ${config.keyHeight};} `; + } + } + + var styleRef = document.createElement("style"); + styleRef.innerHTML = style; + document.head.appendChild(styleRef); +}; + +// Initialize +const initialize = () => { + readConfig(config); + compileKeys(); + wsConnect(); + setInterval(wsWatch, 1000); +}; +initialize(); + +// Update keys on resize +window.onresize = compileKeys; diff --git a/src-tauri/src/bin/test_async.rs b/src-tauri/src/bin/test_async.rs index 9361af2..303424b 100644 --- a/src-tauri/src/bin/test_async.rs +++ b/src-tauri/src/bin/test_async.rs @@ -5,41 +5,41 @@ use std::{future::Future, io, time::Duration}; use tokio::{select, time::sleep}; -use slidershim::slider_io::worker::{AsyncJob, AsyncWorker}; +// use slidershim::slider_io::worker::{AsyncJob, AsyncWorker}; -struct CounterJob; +// struct CounterJob; -#[async_trait] -impl AsyncJob for CounterJob { - async fn do_work + Send>(self, stop_signal: F) { - let job_a = async { - println!("Start job A"); - let mut x = 0; - loop { - x += 1; - println!("{}", x); - sleep(Duration::from_millis(100)).await; - } - }; - let job_b = async move { - println!("Start job B"); - stop_signal.await; - println!("Stop signal hit at job B"); - }; +// #[async_trait] +// impl AsyncJob for CounterJob { +// async fn run + Send>(self, stop_signal: F) { +// let job_a = async { +// println!("Start job A"); +// let mut x = 0; +// loop { +// x += 1; +// println!("{}", x); +// sleep(Duration::from_millis(100)).await; +// } +// }; +// let job_b = async move { +// println!("Start job B"); +// stop_signal.await; +// println!("Stop signal hit at job B"); +// }; - select! { - _ = job_a => {}, - _ = job_b => {}, - } - } -} +// select! { +// _ = job_a => {}, +// _ = job_b => {}, +// } +// } +// } fn main() { env_logger::Builder::new() .filter_level(log::LevelFilter::Debug) .init(); - let worker = AsyncWorker::new("counter", CounterJob); + // let worker = AsyncWorker::new("counter", CounterJob); let mut input = String::new(); let string = io::stdin().read_line(&mut input).unwrap(); } diff --git a/src-tauri/src/bin/test_brokenithm.rs b/src-tauri/src/bin/test_brokenithm.rs index 212517a..f698e38 100644 --- a/src-tauri/src/bin/test_brokenithm.rs +++ b/src-tauri/src/bin/test_brokenithm.rs @@ -4,14 +4,16 @@ use std::{io, time::Duration}; use tokio::time::sleep; -// use slidershim::slider_io::{brokenithm::BrokenithmJob, worker::AsyncWorker}; +use slidershim::slider_io::{ + brokenithm::BrokenithmJob, controller_state::FullState, worker::AsyncWorker, +}; fn main() { env_logger::Builder::new() .filter_level(log::LevelFilter::Debug) .init(); - // let worker = AsyncWorker::new("brokenithm", BrokenithmJob); + let worker = AsyncWorker::new("brokenithm", BrokenithmJob::new(FullState::new())); let mut input = String::new(); let string = io::stdin().read_line(&mut input).unwrap(); } diff --git a/src-tauri/src/build.rs b/src-tauri/src/build.rs index 311900d..14b1182 100644 --- a/src-tauri/src/build.rs +++ b/src-tauri/src/build.rs @@ -1,3 +1,44 @@ +use std::{ + env, fs, + path::{Path, PathBuf}, +}; + +const COPY_DIR: &'static str = "res"; + +fn copy_dir(from: P, to: Q) +where + P: AsRef, + Q: AsRef, +{ + // https://stackoverflow.com/a/68950006 + let to = to.as_ref().to_path_buf(); + + for path in fs::read_dir(from).unwrap() { + let path = path.unwrap().path(); + let to = to.clone().join(path.file_name().unwrap()); + + if path.is_file() { + fs::copy(&path, to).unwrap(); + } else if path.is_dir() { + if !to.exists() { + fs::create_dir(&to).unwrap(); + } + + copy_dir(&path, to); + } else { /* Skip other content */ + } + } +} + fn main() { + let out = env::var("PROFILE").unwrap(); + let out = PathBuf::from(format!("target/{}/{}", out, COPY_DIR)); + + if out.exists() { + fs::remove_dir_all(&out).unwrap() + }; + fs::create_dir(&out).unwrap(); + copy_dir(COPY_DIR, &out); + tauri_build::build(); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5114f56..76e18d8 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -32,8 +32,12 @@ fn main() { env_logger::init(); let config = Arc::new(Mutex::new(Some(slider_io::Config::default()))); + let manager: slider_io::Manager; { - config.lock().unwrap().as_ref().unwrap().save(); + let c = config.lock().unwrap(); + let cr = c.as_ref().unwrap(); + cr.save(); + manager = slider_io::Manager::new(cr.clone()); } tauri::Builder::default() diff --git a/src-tauri/src/slider_io/brokenithm.rs b/src-tauri/src/slider_io/brokenithm.rs index c427c88..676b2e4 100644 --- a/src-tauri/src/slider_io/brokenithm.rs +++ b/src-tauri/src/slider_io/brokenithm.rs @@ -1,50 +1,213 @@ -use std::{convert::Infallible, net::SocketAddr}; - -use log::info; -use tokio::time::sleep; - +use async_trait::async_trait; +use futures::{SinkExt, StreamExt}; use hyper::{ + header, server::conn::AddrStream, service::{make_service_fn, service_fn}, - Body, Request, Response, Server, + upgrade::{self, Upgraded}, + Body, Method, Request, Response, Server, StatusCode, }; +use log::{error, info}; +use path_clean::PathClean; +use std::{convert::Infallible, future::Future, net::SocketAddr, path::PathBuf}; +use tokio::{fs::File, select}; +use tokio_tungstenite::WebSocketStream; +use tokio_util::codec::{BytesCodec, FramedRead}; +use tungstenite::{handshake, Error, Message}; -// use crate::slider_io::worker::{AsyncJob, AsyncJobFut, AsyncJobRecvStop}; +use crate::slider_io::{controller_state::FullState, worker::AsyncJob}; + +// https://levelup.gitconnected.com/handling-websocket-and-http-on-the-same-port-with-rust-f65b770722c9 + +async fn error_response() -> Result, Infallible> { + Ok( + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from(format!("Not found"))) + .unwrap(), + ) +} + +async fn serve_file(path: &str) -> Result, Infallible> { + let mut pb = PathBuf::from("res/www/"); + pb.push(path); + pb.clean(); + + // println!("CWD {:?}", std::env::current_dir()); + // println!("Serving file {:?}", pb); + + match File::open(pb).await { + Ok(f) => { + let stream = FramedRead::new(f, BytesCodec::new()); + let body = Body::wrap_stream(stream); + Ok(Response::new(body)) + } + Err(_) => error_response().await, + } +} + +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; + } + '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"); + break; + } + _ => {} + }, + Err(e) => { + error!("Websocket connection error: {}", e); + break; + } + }, + None => { + break; + } + } + } +} + +async fn handle_websocket( + mut request: Request, + state: FullState, +) -> Result, Infallible> { + let res = match handshake::server::create_response_with_body(&request, || Body::empty()) { + Ok(res) => { + tokio::spawn(async move { + match upgrade::on(&mut request).await { + Ok(upgraded) => { + let ws_stream = WebSocketStream::from_raw_socket( + upgraded, + tokio_tungstenite::tungstenite::protocol::Role::Server, + None, + ) + .await; + + handle_brokenithm(ws_stream, state).await; + } + + Err(e) => { + error!("Websocket upgrade error: {}", e); + } + } + }); + + res + } + Err(e) => { + error!("Websocket creation error: {}", e); + Response::builder() + .status(StatusCode::BAD_REQUEST) + .body(Body::from(format!("Failed to create websocket: {}", e))) + .unwrap() + } + }; + Ok(res) +} async fn handle_request( request: Request, remote_addr: SocketAddr, + state: FullState, ) -> Result, Infallible> { - Ok(Response::new(Body::from(format!( - "Hello there connection {}\n", - remote_addr - )))) -} + let method = request.method(); + let path = request.uri().path(); + if method != Method::GET { + error!("Server unknown method {} {}", method, path); + return error_response().await; + } + info!("Server {} {}", method, path); -async fn brokenithm_server() { - let addr = SocketAddr::from(([0, 0, 0, 0], 1666)); - - info!("Brokenithm opening on {:?}", addr); - - let make_svc = make_service_fn(|conn: &AddrStream| { - let remote_addr = conn.remote_addr(); - async move { - Ok::<_, Infallible>(service_fn(move |request: Request| { - handle_request(request, remote_addr) - })) - } - }); - - let server = Server::bind(&addr).serve(make_svc); - if let Err(e) = server.await { - eprintln!("Server error: {}", e); + match ( + request.uri().path(), + request.headers().contains_key(header::UPGRADE), + ) { + ("/", false) | ("/index.html", false) => serve_file("index.html").await, + (filename, false) => serve_file(&filename[1..]).await, + ("/ws", true) => handle_websocket(request, state).await, + _ => error_response().await, } } -// struct BrokenithmJob; +pub struct BrokenithmJob { + state: FullState, +} -// impl AsyncJob { -// fn job(self, mut recv_stop: AsyncJobRecvStop) -> AsyncJobFut { -// return Box::pin() -// } -// } +impl BrokenithmJob { + pub fn new(state: FullState) -> Self { + Self { state } + } +} + +#[async_trait] +impl AsyncJob for BrokenithmJob { + async fn run + Send>(self, stop_signal: F) { + let state = self.state.clone(); + let make_svc = make_service_fn(|conn: &AddrStream| { + let remote_addr = conn.remote_addr(); + let make_svc_state = state.clone(); + async move { + Ok::<_, Infallible>(service_fn(move |request: Request| { + let svc_state = make_svc_state.clone(); + handle_request(request, remote_addr, svc_state) + })) + } + }); + + let addr = SocketAddr::from(([0, 0, 0, 0], 1606)); + info!("Brokenithm server listening on {}", addr); + + let server = Server::bind(&addr) + // .http1_keepalive(false) + // .http2_keep_alive_interval(None) + // .tcp_keepalive(None) + .serve(make_svc) + .with_graceful_shutdown(stop_signal); + + if let Err(e) = server.await { + info!("Brokenithm server stopped: {}", e); + } + } +} diff --git a/src-tauri/src/slider_io/mod.rs b/src-tauri/src/slider_io/mod.rs index f6a6080..18ab1a6 100644 --- a/src-tauri/src/slider_io/mod.rs +++ b/src-tauri/src/slider_io/mod.rs @@ -2,10 +2,10 @@ mod config; mod utils; pub mod worker; -mod controller_state; +pub mod controller_state; mod voltex; -mod brokenithm; +pub mod brokenithm; mod gamepad; mod keyboard; diff --git a/src-tauri/src/slider_io/worker.rs b/src-tauri/src/slider_io/worker.rs index a3131c8..896cc0e 100644 --- a/src-tauri/src/slider_io/worker.rs +++ b/src-tauri/src/slider_io/worker.rs @@ -68,7 +68,7 @@ impl Drop for ThreadWorker { #[async_trait] pub trait AsyncJob: Send + 'static { - async fn do_work + Send>(self, stop_signal: F); + async fn run + Send>(self, stop_signal: F); } pub struct AsyncWorker { @@ -94,7 +94,7 @@ impl AsyncWorker { let task = runtime.spawn(async move { job - .do_work(async move { + .run(async move { recv_stop.await; }) .await;