refactor to 'lights', add diva protocol

This commit is contained in:
4yn
2022-02-13 23:22:31 +08:00
parent 287c9c63f6
commit f356624502
18 changed files with 401 additions and 123 deletions

File diff suppressed because one or more lines are too long

View File

@@ -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",
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -0,0 +1,120 @@
<!DOCTYPE html>
<html>
<head>
<title>slidershim-brokenithm</title>
<meta charset="utf8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" sizes="192x192" href="/icon.png" />
<style>
#fullscreen {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: #000000;
color: hotpink;
}
.container {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
flex-flow: column nowrap;
touch-action: none;
align-items: stretch;
}
.air-container {
/* display: flex; */
display: none;
flex-flow: column nowrap;
align-items: stretch;
flex: 1;
}
.touch-container {
display: flex;
flex-flow: row nowrap;
align-items: stretch;
flex: 1;
}
.grow > * {
flex: 1;
}
.key {
flex: 1;
border: 1px solid green;
}
.key[data-active] {
background-color: hotpink;
}
.key.air[data-active] {
background-color: skyblue;
}
canvas {
-ms-interpolation-mode: nearest-neighbor;
image-rendering: crisp-edges;
image-rendering: pixelated;
touch-action: none;
margin: 0px -1.5625vw;
}
</style>
</head>
<body>
<div id="fullscreen">
<!-- Offset for LED display -->
<div class="container">
<div class="air-container grow"></div>
<div class="touch-container grow">
<canvas id="canvas" width="33" height="1"></canvas>
</div>
</div>
<!-- Hitbox Divs -->
<div class="container" id="main">
<div class="air-container grow">
<div class="air key" data-air="1" data-kflag="5"></div>
<div class="air key" data-air="1" data-kflag="4"></div>
<div class="air key" data-air="1" data-kflag="3"></div>
<div class="air key" data-air="1" data-kflag="2"></div>
<div class="air key" data-air="1" data-kflag="1"></div>
<div class="air key" data-air="1" data-kflag="0"></div>
</div>
<div class="touch-container grow">
<div class="key" data-kflag="0"></div>
<div class="key" data-kflag="2"></div>
<div class="key" data-kflag="4"></div>
<div class="key" data-kflag="6"></div>
<div class="key" data-kflag="8"></div>
<div class="key" data-kflag="10"></div>
<div class="key" data-kflag="12"></div>
<div class="key" data-kflag="14"></div>
<div class="key" data-kflag="16"></div>
<div class="key" data-kflag="18"></div>
<div class="key" data-kflag="20"></div>
<div class="key" data-kflag="22"></div>
<div class="key" data-kflag="24"></div>
<div class="key" data-kflag="26"></div>
<div class="key" data-kflag="28"></div>
<div class="key" data-kflag="30"></div>
</div>
</div>
</div>
<script src="/config.js"></script>
<script src="/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<head>
<title>slidershim-brokenithm</title>
<meta charset="utf8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="apple-touch-icon" sizes="192x192" href="/icon.png" />
<style>
#fullscreen {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: #000000;
color: hotpink;
}
.container {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
display: flex;
flex-flow: column nowrap;
touch-action: none;
align-items: stretch;
}
.air-container {
display: flex;
flex-flow: column nowrap;
align-items: stretch;
flex: 1;
}
.touch-container {
display: flex;
flex-flow: row nowrap;
align-items: stretch;
flex: 1;
}
.grow > * {
flex: 1;
}
.key {
flex: 1;
border: 1px solid green;
}
.key[data-active] {
background-color: hotpink;
}
.key.air[data-active] {
background-color: skyblue;
}
canvas {
-ms-interpolation-mode: nearest-neighbor;
image-rendering: crisp-edges;
image-rendering: pixelated;
touch-action: none;
margin: 0px -1.5625vw;
}
</style>
</head>
<body>
<div id="fullscreen">
<!-- Offset for LED display -->
<div class="container">
<div class="air-container grow"></div>
<div class="touch-container grow">
<canvas id="canvas" width="33" height="1"></canvas>
</div>
</div>
<!-- Hitbox Divs -->
<div class="container" id="main">
<div class="air-container grow">
<div class="air key" data-air="1" data-kflag="5"></div>
<div class="air key" data-air="1" data-kflag="4"></div>
<div class="air key" data-air="1" data-kflag="3"></div>
<div class="air key" data-air="1" data-kflag="2"></div>
<div class="air key" data-air="1" data-kflag="1"></div>
<div class="air key" data-air="1" data-kflag="0"></div>
</div>
<div class="touch-container grow">
<div class="key" data-kflag="0"></div>
<div class="key" data-kflag="2"></div>
<div class="key" data-kflag="4"></div>
<div class="key" data-kflag="6"></div>
<div class="key" data-kflag="8"></div>
<div class="key" data-kflag="10"></div>
<div class="key" data-kflag="12"></div>
<div class="key" data-kflag="14"></div>
<div class="key" data-kflag="16"></div>
<div class="key" data-kflag="18"></div>
<div class="key" data-kflag="20"></div>
<div class="key" data-kflag="22"></div>
<div class="key" data-kflag="24"></div>
<div class="key" data-kflag="26"></div>
<div class="key" data-kflag="28"></div>
<div class="key" data-kflag="30"></div>
</div>
</div>
</div>
<script src="/config.js"></script>
<script src="/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,347 @@
/*
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 < 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
}
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);
};
// 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;

View File

@@ -0,0 +1,306 @@
use async_trait::async_trait;
use futures::{SinkExt, StreamExt};
use hyper::{
header,
server::conn::AddrStream,
service::{make_service_fn, service_fn},
upgrade::{self, Upgraded},
Body, Method, Request, Response, Server, StatusCode,
};
use log::{error, info};
use phf::phf_map;
use std::{convert::Infallible, future::Future, net::SocketAddr};
use tokio::{
select,
sync::mpsc,
time::{sleep, Duration},
};
use tokio_tungstenite::WebSocketStream;
use tungstenite::{handshake, Message};
use crate::{shared::worker::AsyncHaltableJob, state::SliderState};
// https://levelup.gitconnected.com/handling-websocket-and-http-on-the-same-port-with-rust-f65b770722c9
async fn error_response() -> Result<Response<Body>, Infallible> {
Ok(
Response::builder()
.status(StatusCode::NOT_FOUND)
.body(Body::from(format!("Not found")))
.unwrap(),
)
}
// static x: &'static [u8] = include_bytes!("./brokenithm-www/favicon.ico");
static BROKENITHM_STR_FILES: phf::Map<&'static str, (&'static str, &'static str)> = phf_map! {
"app.js" => (include_str!("./brokenithm-www/app.js"), "text/javascript"),
"config.js" => (include_str!("./brokenithm-www/config.js"), "text/javascript"),
"index-go.html" => (include_str!("./brokenithm-www/index-go.html"), "text/html"),
"index.html" => (include_str!("./brokenithm-www/index.html"), "text/html"),
};
static BROKENITHM_BIN_FILES: phf::Map<&'static str, (&'static [u8], &'static str)> = phf_map! {
"favicon.ico" => (include_bytes!("./brokenithm-www/favicon.ico"), "image/x-icon"),
"icon.png" => (include_bytes!("./brokenithm-www/icon.png"), "image/png"),
};
async fn serve_file(path: &str) -> Result<Response<Body>, Infallible> {
match (
BROKENITHM_STR_FILES.get(path),
BROKENITHM_BIN_FILES.get(path),
) {
(Some((data, mime)), _) => Ok(
Response::builder()
.header(header::CONTENT_TYPE, *mime)
.body(Body::from(*data))
.unwrap(),
),
(_, Some((data, mime))) => Ok(
Response::builder()
.header(header::CONTENT_TYPE, *mime)
.body(Body::from(*data))
.unwrap(),
),
(None, None) => error_response().await,
}
}
async fn handle_brokenithm(
ws_stream: WebSocketStream<Upgraded>,
state: SliderState,
lights_enabled: bool,
) {
let (mut ws_write, mut ws_read) = ws_stream.split();
let (msg_write, mut msg_read) = mpsc::unbounded_channel::<Message>();
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;
}
},
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 chars = msg.chars().collect::<Vec<char>>();
match chars.len() {
6 => {
if chars[0] == 'a' {
msg_write_handle
.send(Message::Text("alive".to_string()))
.ok();
}
}
39 => {
if chars[0] == 'b' {
let mut input_handle = state_handle.input.lock();
for (idx, c) in chars[0..32].iter().enumerate() {
input_handle.ground[idx] = match *c == '1' {
false => 0,
true => 255,
}
}
for (idx, c) in chars[32..38].iter().enumerate() {
input_handle.air[idx] = match *c == '1' {
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");
};
match lights_enabled {
false => {
select! {
_ = read_task => {}
_ = write_task => {}
};
}
true => {
let msg_write_handle = msg_write.clone();
let state_handle = state.clone();
let lights_task = async move {
loop {
let mut lights_data = vec![0; 93];
{
let lights_handle = state_handle.lights.lock();
(&mut lights_data).copy_from_slice(&lights_handle.ground);
}
msg_write_handle.send(Message::Binary(lights_data)).ok();
sleep(Duration::from_millis(50)).await;
}
};
select! {
_ = read_task => {}
_ = write_task => {}
_ = lights_task => {}
};
}
}
}
async fn handle_websocket(
mut request: Request<Body>,
state: SliderState,
lights_enabled: bool,
) -> Result<Response<Body>, 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, lights_enabled).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<Body>,
remote_addr: SocketAddr,
state: SliderState,
ground_only: bool,
lights_enabled: bool,
) -> Result<Response<Body>, Infallible> {
let method = request.method();
let path = request.uri().path();
if method != Method::GET {
error!(
"Server unknown method {} -> {} {}",
remote_addr, method, path
);
return error_response().await;
}
info!("Server {} -> {} {}", remote_addr, method, path);
match (
request.uri().path(),
request.headers().contains_key(header::UPGRADE),
) {
("/", false) | ("/index.html", false) => match ground_only {
false => serve_file("index.html").await,
true => serve_file("index-go.html").await,
},
(filename, false) => serve_file(&filename[1..]).await,
("/ws", true) => handle_websocket(request, state, lights_enabled).await,
_ => error_response().await,
}
}
pub struct BrokenithmJob {
state: SliderState,
ground_only: bool,
lights_enabled: bool,
}
impl BrokenithmJob {
pub fn new(state: &SliderState, ground_only: &bool, lights_enabled: &bool) -> Self {
Self {
state: state.clone(),
ground_only: *ground_only,
lights_enabled: *lights_enabled,
}
}
}
#[async_trait]
impl AsyncHaltableJob for BrokenithmJob {
async fn run<F: Future<Output = ()> + Send>(self, stop_signal: F) {
let state = self.state.clone();
let ground_only = self.ground_only;
let lights_enabled = self.lights_enabled;
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<Body>| {
let svc_state = make_svc_state.clone();
handle_request(request, remote_addr, svc_state, ground_only, lights_enabled)
}))
}
});
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);
}
}
}

View File

@@ -0,0 +1,60 @@
use serde_json::Value;
#[derive(Debug, Clone)]
pub enum HardwareSpec {
TasollerOne,
TasollerTwo,
Yuancon,
}
#[derive(Debug, Clone)]
pub enum DeviceMode {
None,
Hardware {
spec: HardwareSpec,
},
Brokenithm {
ground_only: bool,
lights_enabled: bool,
},
DivaSlider {
port: String,
},
}
impl DeviceMode {
pub fn from_serde_value(v: &Value) -> Option<Self> {
Some(match v["deviceMode"].as_str()? {
"none" => DeviceMode::None,
"tasoller-one" => DeviceMode::Hardware {
spec: HardwareSpec::TasollerOne,
},
"tasoller-two" => DeviceMode::Hardware {
spec: HardwareSpec::TasollerTwo,
},
"yuancon" => DeviceMode::Hardware {
spec: HardwareSpec::Yuancon,
},
"diva" => DeviceMode::DivaSlider {
port: v["divaSerialPort"].as_str()?.to_string(),
},
"brokenithm" => DeviceMode::Brokenithm {
ground_only: false,
lights_enabled: false,
},
"brokenithm-led" => DeviceMode::Brokenithm {
ground_only: false,
lights_enabled: true,
},
"brokenithm-ground" => DeviceMode::Brokenithm {
ground_only: true,
lights_enabled: false,
},
"brokenithm-ground-led" => DeviceMode::Brokenithm {
ground_only: true,
lights_enabled: true,
},
_ => return None,
})
}
}

View File

@@ -0,0 +1,313 @@
use log::{error, info};
use serialport::SerialPort;
use std::{collections::VecDeque, num::Wrapping, thread::sleep, time::Duration};
use crate::{shared::worker::ThreadJob, state::SliderState};
struct DivaPacket {
command: u8,
len: u8,
data: Vec<u8>,
checksum: Wrapping<u8>,
raw: Option<Vec<u8>>,
}
impl DivaPacket {
fn new() -> Self {
Self {
command: 0,
len: 0,
data: Vec::with_capacity(256),
checksum: Wrapping(0),
raw: None,
}
}
fn from_bytes(command: u8, data: &[u8]) -> Self {
Self {
command,
len: data.len() as u8,
data: data.iter().copied().collect(),
checksum: Wrapping(0),
raw: None,
}
}
fn push_raw_escaped(byte: u8, raw: &mut Vec<u8>) {
match byte {
0xfd => {
raw.push(0xfd);
raw.push(0xfc);
}
0xff => {
raw.push(0xfd);
raw.push(0xfe);
}
_ => {
raw.push(byte);
}
}
}
fn serialize(&mut self) -> &[u8] {
let mut raw: Vec<u8> = Vec::with_capacity(512);
let mut checksum = Wrapping(0);
raw.push(0xff);
checksum += Wrapping(0xffu8);
Self::push_raw_escaped(self.command, &mut raw);
checksum += Wrapping(self.command);
Self::push_raw_escaped(self.len, &mut raw);
checksum += Wrapping(self.len);
for i in &self.data {
Self::push_raw_escaped(*i, &mut raw);
checksum += Wrapping(*i);
}
checksum = -checksum;
Self::push_raw_escaped(checksum.0, &mut raw);
self.raw = Some(raw);
return self.raw.as_ref().unwrap();
}
}
enum DivaDeserializerState {
ExpectCommand,
ExpectLen,
ExpectData,
ExpectChecksum,
Done,
}
struct DivaDeserializer {
state: DivaDeserializerState,
escape: u8,
len: u8,
packet: DivaPacket,
}
impl DivaDeserializer {
fn new() -> Self {
Self {
state: DivaDeserializerState::Done,
escape: 1,
len: 0,
packet: DivaPacket::new(),
}
}
fn deserialize(&mut self, data: &[u8], out: &mut VecDeque<DivaPacket>) {
for c in data {
match c {
0xff => {
self.packet = DivaPacket::new();
self.packet.checksum = Wrapping(0xff);
self.state = DivaDeserializerState::ExpectCommand;
}
0xfd => {
self.escape = 1;
}
c => {
let c = c + self.escape;
self.escape = 0;
self.packet.checksum += Wrapping(c);
match self.state {
DivaDeserializerState::ExpectCommand => {
self.packet.command = c;
self.state = DivaDeserializerState::ExpectLen;
}
DivaDeserializerState::ExpectLen => {
self.len = c;
self.packet.len = c;
self.state = match c {
0 => DivaDeserializerState::ExpectChecksum,
_ => DivaDeserializerState::ExpectData,
};
}
DivaDeserializerState::ExpectData => {
self.packet.data.push(c);
self.len -= 1;
if self.len == 0 {
self.state = DivaDeserializerState::ExpectChecksum;
}
}
DivaDeserializerState::ExpectChecksum => {
debug_assert!(self.packet.checksum == Wrapping(0));
if self.packet.checksum == Wrapping(0) {
out.push_back(DivaPacket::new());
std::mem::swap(&mut self.packet, out.back_mut().unwrap());
}
self.state = DivaDeserializerState::Done;
}
_ => {}
}
}
}
}
}
}
enum DivaSliderBootstrap {
Init,
AwaitReset,
AwaitInfo,
AwaitStart,
ReadLoop,
}
pub struct DivaSliderJob {
state: SliderState,
port: String,
packets: VecDeque<DivaPacket>,
deserializer: DivaDeserializer,
serial_port: Option<Box<dyn SerialPort>>,
bootstrap: DivaSliderBootstrap,
}
impl DivaSliderJob {
pub fn new(state: &SliderState, port: &String) -> Self {
Self {
state: state.clone(),
port: port.clone(),
packets: VecDeque::with_capacity(100),
deserializer: DivaDeserializer::new(),
serial_port: None,
bootstrap: DivaSliderBootstrap::Init,
}
}
}
impl ThreadJob for DivaSliderJob {
fn setup(&mut self) -> bool {
info!(
"Serial port for diva slider opening at {} {:?}",
self.port.as_str(),
115200
);
match serialport::new(&self.port, 152000).open() {
Ok(serial_port_buf) => {
info!("Serial port opened");
self.serial_port = Some(serial_port_buf);
true
}
Err(e) => {
error!("Serial port could not open: {}", e);
false
}
}
}
fn tick(&mut self) -> bool {
let mut work = false;
let serial_port = self.serial_port.as_mut().unwrap();
let bytes_avail = serial_port.bytes_to_read().unwrap_or(0);
if bytes_avail > 0 {
let mut read_buf = vec![0 as u8; bytes_avail as usize];
serial_port.read_exact(&mut read_buf).ok();
self.deserializer.deserialize(&read_buf, &mut self.packets);
work = true;
}
match self.bootstrap {
DivaSliderBootstrap::Init => {
let mut reset_packet = DivaPacket::from_bytes(0x10, &[]);
serial_port.write(reset_packet.serialize()).ok();
self.bootstrap = DivaSliderBootstrap::AwaitReset;
work = true;
}
DivaSliderBootstrap::AwaitReset => {
if let Some(ack_packet) = self.packets.pop_front() {
info!(
"Diva slider ack reset {:?} {:?}",
ack_packet.command, ack_packet.data
);
let mut info_packet = DivaPacket::from_bytes(0xf0, &[]);
serial_port.write(info_packet.serialize()).ok();
self.bootstrap = DivaSliderBootstrap::AwaitInfo;
work = true;
}
}
DivaSliderBootstrap::AwaitInfo => {
if let Some(ack_packet) = self.packets.pop_front() {
info!(
"Diva slider ack info {:?} {:?}",
ack_packet.command, ack_packet.data
);
let mut start_packet = DivaPacket::from_bytes(0x03, &[]);
serial_port.write(start_packet.serialize()).ok();
self.bootstrap = DivaSliderBootstrap::AwaitStart;
work = true;
}
}
DivaSliderBootstrap::AwaitStart => {
if let Some(ack_packet) = self.packets.pop_front() {
info!(
"Diva slider ack start {:?} {:?}",
ack_packet.command, ack_packet.data
);
self.bootstrap = DivaSliderBootstrap::ReadLoop;
work = true;
}
}
DivaSliderBootstrap::ReadLoop => {
while let Some(data_packet) = self.packets.pop_front() {
if data_packet.command == 0x01 && data_packet.len == 32 {
let mut input_handle = self.state.input.lock();
input_handle
.ground
.copy_from_slice(&data_packet.data[0..32]);
work = true;
}
}
let mut send_lights = false;
let mut lights_buf = [0; 97];
{
let mut lights_handle = self.state.lights.lock();
if lights_handle.dirty {
send_lights = true;
lights_buf[0] = 0x3f;
lights_buf[1..97].copy_from_slice(&lights_handle.ground[0..96]);
lights_handle.dirty = false;
}
}
if send_lights {
let mut lights_packet = DivaPacket::from_bytes(0x02, &lights_buf);
serial_port.write(lights_packet.serialize()).ok();
}
}
};
// TODO: async worker?
sleep(Duration::from_millis(10));
work
}
}
impl Drop for DivaSliderJob {
fn drop(&mut self) {
match self.bootstrap {
DivaSliderBootstrap::AwaitStart | DivaSliderBootstrap::ReadLoop => {
info!("Diva slider sending stop");
let serial_port = self.serial_port.as_mut().unwrap();
let mut stop_packet = DivaPacket::from_bytes(0x04, &[]);
serial_port.write(stop_packet.serialize()).ok();
}
_ => {}
};
}
}

View File

@@ -0,0 +1,292 @@
use log::{error, info};
use rusb::{self, DeviceHandle, GlobalContext};
use std::{
error::Error,
mem::swap,
ops::{Deref, DerefMut},
time::Duration,
};
use crate::{
shared::{
utils::{Buffer, ShimError},
worker::ThreadJob,
},
state::{SliderInput, SliderLights, SliderState},
};
use super::config::HardwareSpec;
type HidReadCallback = fn(&Buffer, &mut SliderInput) -> ();
type HidLedCallback = fn(&mut Buffer, &SliderLights) -> ();
enum WriteType {
Bulk,
Interrupt,
}
pub struct HidJob {
state: SliderState,
vid: u16,
pid: u16,
read_endpoint: u8,
led_endpoint: u8,
read_callback: HidReadCallback,
read_buf: Buffer,
last_read_buf: Buffer,
led_write_type: WriteType,
led_callback: HidLedCallback,
led_buf: Buffer,
handle: Option<DeviceHandle<GlobalContext>>,
}
impl HidJob {
fn new(
state: SliderState,
vid: u16,
pid: u16,
read_endpoint: u8,
led_endpoint: u8,
read_callback: HidReadCallback,
led_type: WriteType,
led_callback: HidLedCallback,
) -> Self {
Self {
state,
vid,
pid,
read_endpoint,
led_endpoint,
read_callback,
read_buf: Buffer::new(),
last_read_buf: Buffer::new(),
led_write_type: led_type,
led_callback,
led_buf: Buffer::new(),
handle: None,
}
}
pub fn from_config(state: &SliderState, spec: &HardwareSpec) -> Self {
match spec {
HardwareSpec::TasollerOne => Self::new(
state.clone(),
0x1ccf,
0x2333,
0x84,
0x03,
|buf, input| {
if buf.len != 11 {
return;
}
let bits: Vec<u8> = buf
.data
.iter()
.flat_map(|x| (0..8).map(move |i| ((x) >> i) & 1))
.collect();
for i in 0..32 {
input.ground[i] = bits[34 + i] * 255;
}
input.flip_vert();
input.air.copy_from_slice(&bits[28..34]);
input.extra[0..2].copy_from_slice(&bits[26..28]);
},
WriteType::Bulk,
|buf, lights| {
buf.len = 240;
buf.data[0] = 'B' as u8;
buf.data[1] = 'L' as u8;
buf.data[2] = '\x00' as u8;
for (buf_chunk, state_chunk) in buf.data[3..96]
.chunks_mut(3)
.take(31)
.zip(lights.ground.chunks(3).rev())
{
buf_chunk[0] = state_chunk[1];
buf_chunk[1] = state_chunk[0];
buf_chunk[2] = state_chunk[2];
}
buf.data[96..240].fill(0);
},
),
HardwareSpec::TasollerTwo => Self::new(
state.clone(),
0x1ccf,
0x2333,
0x84,
0x03,
|buf, input| {
if buf.len != 36 {
return;
}
input.ground.copy_from_slice(&buf.data[4..36]);
input.flip_vert();
let bits: Vec<u8> = (0..8).map(|x| (buf.data[3] >> x) & 1).collect();
input.air.copy_from_slice(&bits[0..6]);
input.extra[0..2].copy_from_slice(&bits[6..8]);
},
WriteType::Bulk,
|buf, lights| {
buf.len = 240;
buf.data[0] = 'B' as u8;
buf.data[1] = 'L' as u8;
buf.data[2] = '\x00' as u8;
for (buf_chunk, state_chunk) in buf.data[3..96]
.chunks_mut(3)
.take(31)
.zip(lights.ground.chunks(3).rev())
{
buf_chunk[0] = state_chunk[1];
buf_chunk[1] = state_chunk[0];
buf_chunk[2] = state_chunk[2];
}
buf.data[96..240].fill(0);
},
),
HardwareSpec::Yuancon => Self::new(
state.clone(),
0x1973,
0x2001,
0x81,
0x02,
|buf, input| {
if buf.len != 34 {
return;
}
input.ground.copy_from_slice(&buf.data[2..34]);
for i in 0..6 {
input.air[i ^ 1] = (buf.data[0] >> i) & 1;
}
for i in 0..3 {
input.extra[2 - i] = (buf.data[1] >> i) & 1;
}
},
WriteType::Interrupt,
|buf, lights| {
buf.len = 31 * 2;
for (buf_chunk, state_chunk) in buf
.data
.chunks_mut(2)
.take(31)
.zip(lights.ground.chunks(3).rev())
{
buf_chunk[0] = (state_chunk[0] << 3 & 0xe0) | (state_chunk[2] >> 3);
buf_chunk[1] = (state_chunk[1] & 0xf8) | (state_chunk[0] >> 5);
}
},
),
}
}
fn get_handle(&mut self) -> Result<(), Box<dyn Error>> {
info!("Device finding vid {} pid {}", self.vid, self.pid);
let handle = rusb::open_device_with_vid_pid(self.vid, self.pid);
if handle.is_none() {
error!("Device not found");
return Err(Box::new(ShimError));
}
let mut handle = handle.unwrap();
info!("Device found {:?}", handle);
if handle.kernel_driver_active(0).unwrap_or(false) {
info!("Device detaching kernel driver");
handle.detach_kernel_driver(0)?;
}
info!("Device setting configuration");
handle.set_active_configuration(1)?;
info!("Device claiming interface");
handle.claim_interface(0)?;
self.handle = Some(handle);
Ok(())
}
}
const TIMEOUT: Duration = Duration::from_millis(20);
impl ThreadJob for HidJob {
fn setup(&mut self) -> bool {
match self.get_handle() {
Ok(_) => {
info!("Device OK");
true
}
Err(e) => {
error!("Device setup failed: {}", e);
false
}
}
}
fn tick(&mut self) -> bool {
// Input loop
let handle = self.handle.as_mut().unwrap();
let mut work = false;
{
let res = handle
.read_interrupt(self.read_endpoint, &mut self.read_buf.data, TIMEOUT)
.map_err(|e| {
// debug!("Device read error {}", &e);
e
})
.unwrap_or(0);
self.read_buf.len = res;
// debug!("{:?}", self.read_buf.slice());
// if self.read_buf.len != 0 {
if (self.read_buf.len != 0) && (self.read_buf.slice() != self.last_read_buf.slice()) {
work = true;
let mut input_handle = self.state.input.lock();
(self.read_callback)(&self.read_buf, input_handle.deref_mut());
swap(&mut self.read_buf, &mut self.last_read_buf);
}
}
// Led loop
{
{
let mut lights_handle = self.state.lights.lock();
if lights_handle.dirty {
(self.led_callback)(&mut self.led_buf, lights_handle.deref());
lights_handle.dirty = false;
}
}
if self.led_buf.len != 0 {
let res = (match self.led_write_type {
WriteType::Bulk => handle.write_bulk(self.led_endpoint, self.led_buf.slice(), TIMEOUT),
WriteType::Interrupt => {
handle.write_interrupt(self.led_endpoint, &self.led_buf.slice(), TIMEOUT)
}
})
.map_err(|e| {
// debug!("Device write error {}", e);
e
})
.unwrap_or(0);
if res == self.led_buf.len + 1 {
// work = true;
self.led_buf.len = 0;
}
}
}
work
}
}
impl Drop for HidJob {
fn drop(&mut self) {
if let Some(handle) = self.handle.as_mut() {
handle.release_interface(0).ok();
}
}
}

View File

@@ -0,0 +1,5 @@
pub mod config;
pub mod diva;
pub mod brokenithm;
pub mod hid;