mirror of
https://github.com/4yn/slidershim.git
synced 2026-09-07 16:25:40 -05:00
refactor to 'lights', add diva protocol
This commit is contained in:
1
src-slider_io/src/device/brokenithm-www/app.js
Normal file
1
src-slider_io/src/device/brokenithm-www/app.js
Normal file
File diff suppressed because one or more lines are too long
21
src-slider_io/src/device/brokenithm-www/config.js
Normal file
21
src-slider_io/src/device/brokenithm-www/config.js
Normal 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",
|
||||
};
|
||||
BIN
src-slider_io/src/device/brokenithm-www/favicon.ico
Normal file
BIN
src-slider_io/src/device/brokenithm-www/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src-slider_io/src/device/brokenithm-www/icon.png
Normal file
BIN
src-slider_io/src/device/brokenithm-www/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
120
src-slider_io/src/device/brokenithm-www/index-go.html
Normal file
120
src-slider_io/src/device/brokenithm-www/index-go.html
Normal 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>
|
||||
119
src-slider_io/src/device/brokenithm-www/index.html
Normal file
119
src-slider_io/src/device/brokenithm-www/index.html
Normal 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>
|
||||
347
src-slider_io/src/device/brokenithm-www/src.js
Normal file
347
src-slider_io/src/device/brokenithm-www/src.js
Normal 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;
|
||||
306
src-slider_io/src/device/brokenithm.rs
Normal file
306
src-slider_io/src/device/brokenithm.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src-slider_io/src/device/config.rs
Normal file
60
src-slider_io/src/device/config.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
313
src-slider_io/src/device/diva.rs
Normal file
313
src-slider_io/src/device/diva.rs
Normal 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();
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
292
src-slider_io/src/device/hid.rs
Normal file
292
src-slider_io/src/device/hid.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src-slider_io/src/device/mod.rs
Normal file
5
src-slider_io/src/device/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod config;
|
||||
|
||||
pub mod diva;
|
||||
pub mod brokenithm;
|
||||
pub mod hid;
|
||||
Reference in New Issue
Block a user