46 Commits
Author SHA1 Message Date
4yn d02990d5fc update readme 2022-02-22 23:26:21 +08:00
4yn dd98fe5579 remove debugging 2022-02-22 23:15:45 +08:00
4yn 7178e614b1 bump version 2022-02-22 23:15:34 +08:00
4yn 0029dad2f2 flip slider input and rgb order 2022-02-22 23:08:51 +08:00
4yn 08b1b92f52 hook with slider 2022-02-22 21:55:39 +08:00
4yn 24d920c5bf migrate to c++ serial lib 2022-02-22 10:46:13 +08:00
4yn f396a21999 Add 16 key output 2022-02-19 13:55:38 +08:00
4yn 2782d406dd wip serial 2022-02-17 01:40:48 +08:00
4yn 5865cd9d21 diva draft 2022-02-14 02:04:06 +08:00
4yn 6bc462210f diva serial ui 2022-02-13 23:39:54 +08:00
4yn f356624502 refactor to 'lights', add diva protocol 2022-02-13 23:22:31 +08:00
4yn 287c9c63f6 refactor input and system module 2022-02-13 16:35:26 +08:00
4yn ac4933449b refactor state structs 2022-02-13 15:50:03 +08:00
4yn 2a0d466cf3 rename flat to to_flat 2022-02-13 15:27:32 +08:00
4yn 0772285761 Controller state docs 2022-02-13 15:27:07 +08:00
4yn 2539b9ad23 Update readme 2022-02-12 23:54:06 +08:00
4yn 2d56431fe2 split up json parsing logic 2022-02-12 23:43:28 +08:00
4yn 4dbeb432ec restructure 2022-02-12 23:23:22 +08:00
4yn 4f4d951610 restructuring 2022-02-12 22:14:50 +08:00
4yn 3d20472c7f whoops 2022-02-09 22:59:28 +08:00
4yn c502941251 increase ss size 2022-02-09 22:37:01 +08:00
4yn 885ac1e77c license 2022-02-09 22:32:01 +08:00
4yn e2dec75e6b readme 2022-02-09 22:30:10 +08:00
4yn 2ae7a81f22 async output and led 2022-02-09 14:00:59 +08:00
4yn afa5a46c65 some error handling 2022-02-07 15:56:03 +08:00
4yn 6f052554ce update icon 2022-02-07 13:01:52 +08:00
4yn da00ed9afa new icon 2022-02-07 12:50:26 +08:00
4yn 51a77c76fa mimetype and kyeboard 2022-02-07 12:41:45 +08:00
4yn 3ba7f2b8f2 disable right click 2022-02-07 11:55:08 +08:00
4yn 331d23a674 update todo 2022-02-07 11:48:51 +08:00
4yn 3acf511da8 static include 2022-02-07 11:47:11 +08:00
4yn 98509250d6 timer 2022-02-07 10:10:36 +08:00
4yn a5c89f00cb brokenithm leds 2022-02-07 03:38:48 +08:00
4yn cc24e628aa better brokenithm 2022-02-07 02:01:34 +08:00
4yn fcf80538b4 qol and bugfix 2022-02-06 17:56:50 +08:00
4yn a8d72207a4 naming 2022-02-05 18:07:18 +08:00
4yn 21afaa661b brokenithm urls and logging 2022-02-05 18:00:06 +08:00
4yn 91030cb96b bundle 2022-02-05 14:45:25 +08:00
4yn 8a67094c9a Frontend polling 2022-02-05 14:16:14 +08:00
4yn bfb7f7dc95 fix warnings 2022-02-05 11:44:42 +08:00
4yn 263d21c0a6 integration hell 2022-02-05 04:45:34 +08:00
4yn 786ac83c4c brokenithm working 2022-02-05 04:22:29 +08:00
4yn 7f155903ca better async 2022-02-05 00:40:50 +08:00
4yn 281e4b02b1 Async worker 2022-02-04 23:45:36 +08:00
4yn 93240ef4af voltex hid controller 2022-02-02 15:14:56 +08:00
4yn 2b20902179 voltex 2022-02-01 10:37:53 +08:00
110 changed files with 22263 additions and 1708 deletions
+79 -2
View File
@@ -1,3 +1,80 @@
{
"editor.formatOnSave": true
}
"editor.formatOnSave": true,
"cmake.sourceDirectory": "${workspaceFolder}/src-serial",
"cmake.buildDirectory": "${workspaceFolder}/src-serial/build",
"files.associations": {
"xstring": "cpp",
"algorithm": "cpp",
"atomic": "cpp",
"bit": "cpp",
"cctype": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"compare": "cpp",
"concepts": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"exception": "cpp",
"initializer_list": "cpp",
"ios": "cpp",
"iosfwd": "cpp",
"istream": "cpp",
"iterator": "cpp",
"limits": "cpp",
"memory": "cpp",
"new": "cpp",
"ostream": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"string": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"typeinfo": "cpp",
"utility": "cpp",
"vector": "cpp",
"xfacet": "cpp",
"xiosbase": "cpp",
"xlocale": "cpp",
"xlocinfo": "cpp",
"xlocnum": "cpp",
"xmemory": "cpp",
"xstddef": "cpp",
"xtr1common": "cpp",
"xutility": "cpp",
"array": "cpp",
"functional": "cpp",
"list": "cpp",
"set": "cpp",
"unordered_map": "cpp",
"xhash": "cpp",
"xtree": "cpp",
"charconv": "cpp",
"chrono": "cpp",
"complex": "cpp",
"deque": "cpp",
"format": "cpp",
"forward_list": "cpp",
"iomanip": "cpp",
"iostream": "cpp",
"locale": "cpp",
"map": "cpp",
"mutex": "cpp",
"numeric": "cpp",
"optional": "cpp",
"queue": "cpp",
"ratio": "cpp",
"stop_token": "cpp",
"thread": "cpp",
"xlocbuf": "cpp",
"xlocmes": "cpp",
"xlocmon": "cpp",
"xloctime": "cpp"
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 4yn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+134 -8
View File
@@ -1,9 +1,135 @@
# Todo
<img align="right" alt="logo" width="100" height="100" src="public/icon.png">
- input polling rate counter
- led polling rate settings
- settings serializer
- brokenithm server
- ouptut websocket
- led websocket
- comments
# [slidershim](https://github.com/4yn/slidershim)
Software adapter for various Chunithm slider controllers with a built-in Brokenithm web controller. Has support for keyboard/gamepad output and LED lighting.
[Download link](https://github.com/4yn/slidershim/releases/latest)
> Starting 0.2.0, there are reports that windows defender may block this software. You may need to disable windows defender the first time you run slidershim. If you are not comfortable with this, you can [download an older version here](https://github.com/4yn/slidershim/releases/tag/v0.1.4) that is known to be OK with windows defender.
<img alt="screenshot" src="res/screenshots/demo.png">
## Changelog
- v0.2.0
- Added support for Hori Project DIVA slider protocol
- Added 16 key keyboard layout
- v0.1.4
- Initial public release
## Setup Instructions
1. [Download here](https://github.com/4yn/slidershim/releases/latest)
2. Extract to any location you like.
3. Run `slidershim.exe`.
4. Pick your input device, output method and preferred lighting.
5. Click apply.
6. ???
7. Profit!
### Extra Pointers
- Less resources are used when the configuration window is hidden.
- Un-hide the configuration window from the system tray.
- For gamepad output, you will need to install [ViGEmBus](https://github.com/ViGEm/ViGEmBus/releases/latest) for it to work.
- For serial LED input, you may need to install [com0com](https://sourceforge.net/projects/com0com/files/com0com/2.2.2.0/) to bridge slidershim with the LED data stream.
### Brokenithm Setup
1. Set the device option to "Brokenithm" and apply.
2. Allow firewall access to `slidershim.exe`
3. Connect a touch device to the same wifi or local network as your windows machine
> Windows hotspot works too, but make sure to give "public" firewall access
4. Open the web controller in your tablet device web browser. One of the links should work.
1. Either manually type in the IP address if you know what IP it is;
2. Or scan the generated "Broknenithm QR" to get a list of clickable links and save some typing.
5. (Optional) Add the webpage to home screen and/or enable guided access to avoid accidentally exiting the web controller.
## Output Layouts
![layouts](./res/layouts/layout.png)
Voltex layout disables air tower, Neardayo layout enables air tower.
## Troubleshooting
<details>
<summary><strong>My controller is not recognised</strong></summary>
- slidershim supports the GAMO2 Tasoller and Yuancon Laverita v2 controllers on HID firmware.
- If you are using a Tasoller controller, make sure it has the correct HID firmware installed.
- If your hardware controller is not one of these, it will not work.
- If you have a device that communicates using serial protocol, chances are it will work with the "Diva Slider over Serial" option
- Hotplug is not supported. If you plugged in the controller after starting slidershim, re-connect your controller by just clicking the "Apply" button (even if it is grey).
</details>
<details>
<summary><strong>I am using a controller and my keyboard buttons are pressed too often</strong></summary>
- Adjust the sensitivity options.
- The higher the number, the harder it is for keyboard buttons to be pressed.
</details>
<details>
<summary><strong>The Brokenithm controller cannot be loaded</strong></summary>
- Make sure that you can load the controller in your web browser _from the same windows machine that is running slidershim_.
- Close the windows machine web browser afterwards once you have tested it.
- Double check that your tablet device is connected to the same wifi or local network.
- Try restarting slidershim.
</details>
<details>
<summary><strong>The Brokenithm controller is stuck</strong></summary>
- Applying changes to the slidershim configuration will reset the brokenithm controller.
- Refresh the web page or force-stop the controller from your tablet device's task switcher and re-open the controller.
</details>
<details>
<summary><strong>slidershim 2.0 crashes right after I turn it on</strong></summary>
I'm not sure why this happens. I suspect some cases are because of windows defender and other antivirus software stopping the process while running. Try disabling antivirus or using an [older version of slidershim](https://github.com/4yn/slidershim/releases/tag/v0.1.4).
</details>
<details>
<summary><strong>I am still having problems</strong></summary>
- Join the [Cons&amp;Stuff Discord](https://discord.com/invite/zFGemg4r) and ping me there.
- Try to bring your logs.
</details>
<details>
<summary><strong>Neardayo layout?</strong></summary>
- [Neardayo layout.](https://youtu.be/8dA-RCSB-qA?t=25)
</details>
## Technical Information
- Built with Rust, [tauri](https://github.com/tauri-apps/tauri) + [Svelte](https://github.com/sveltejs/svelte), [rusb](https://github.com/a1ien/rusb), [vigem](https://github.com/ViGEm/ViGEmClient)[-client](https://github.com/CasualX/vigem-client), [hyper](https://github.com/hyperium/hyper/), [tokio-](https://github.com/snapview/tokio-tungstenite)[tungstenite](https://github.com/snapview/tungstenite-rs), [serialport](https://crates.io/crates/serialport), [dtolnay/cxx](https://github.com/dtolnay/cxx) and [wjwwood/serial](https://github.com/wjwwood/serial)
- USB device and serial polling is done on a dedicated thread while everything else is done on a async runtime.
### Repository Layout
- [`public`](./public) / [`src`](./src): Svelte frontend
- [`src-tauri`](./src-tauri): Tauri adapter between frontend and backend
- [`src-slider_io`](./src-slider_io): Backend logic for handling usb, brokenithm, output emulation and lighting.
- [`res`](./res): Miscellaneous tools such as Brokenithm QR parser and keyboard layout visualisation.
## Planned Features
- Major:
- ✅ DONE: Support AC slider serial protocol
- Output and LED websocket adapters when UMIGURI comes out
- QOL:
- Documentation
- Comprehensive logging and error handling
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

+5
View File
@@ -0,0 +1,5 @@
```
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
$env:RUST_BACKTRACE = "1";
yarn tauri dev
```
+11683
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -28,6 +28,7 @@
},
"dependencies": {
"@tauri-apps/api": "^1.0.0-beta.8",
"sharp": "^0.30.0",
"sirv-cli": "^2.0.0"
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

+132 -13
View File
@@ -1,4 +1,5 @@
html, body {
html,
body {
position: relative;
width: 100%;
height: 100%;
@@ -9,29 +10,79 @@ html, body {
* {
box-sizing: border-box;
color: #ddd;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
font-size: 1rem;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
/* font-size: 1rem; */
user-select: none;
}
pre {
font-family: monospace;
}
body {
margin: 0;
padding: 1rem;
border: 0.125rem solid #333;
}
/* titlebar */
.titlebar {
user-select: none;
background: #333;
display: flex;
align-items: center;
justify-content: space-between;
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 0.25rem 0.5rem;
height: 2rem;
}
.titlebar-front {
background: #0000;
}
.header-icon {
max-height: 100%;
flex: 0 0 auto;
}
.header-icon img {
max-height: 1.5rem;
pointer-events: none;
}
.header {
font-size: 1.5rem;
font-weight: 500;
flex: 0 0 auto;
}
.header-space {
flex: 1 0 auto;
}
.header-timer {
flex: 0 0 auto;
font-family: monospace;
}
/* main */
.main {
margin-top: 2rem;
display: flex;
flex-flow: column nowrap;
align-items: stretch;
justify-content: flex-start;
}
.header {
font-size: 2rem;
font-weight: 500;
}
.row, .row-2 {
.row,
.row-2 {
margin: 0 0 0.5rem 0;
display: flex;
flex-flow: row nowrap;
@@ -43,15 +94,32 @@ body {
flex: 1 1 0;
}
.row .label[title] {
text-decoration: underline dotted;
}
.row .label[title]:hover {
text-decoration: underline;
}
.row .input {
flex: 2 2 0;
}
input, select {
.serverlist {
width: 100%;
max-height: 5rem;
overflow-x: auto;
font-size: 0.75rem;
}
input,
select {
width: 100%;
background-color: #444;
color: #ddd;
border: none;
font-size: 1rem;
}
button {
@@ -71,22 +139,53 @@ button:active {
background: #777;
}
button.primary {
background: rgb(35, 67, 211);
}
/* Preview */
.preview {
width: 100%;
display: flex;
flex-flow: column nowrap;
align-items: stretch;
justify-content: flex-start;
/* border: 0.125rem solid white; */
}
.air {
height: 2rem;
display: flex;
flex-flow: column-reverse nowrap;
align-items: stretch;
justify-content: flex-start;
border-radius: 0.5rem 0.5rem 0 0;
overflow: clip;
}
.air-data {
flex: 1 0;
}
.air-data-0 {
background: #000;
}
.air-data-1 {
background: #0aa;
}
.ground {
position: relative;
height: 3rem;
border-radius: 0 0 0.5rem 0.5rem;
overflow: clip;
}
.ground-btn, .ground-led {
.ground-btn,
.ground-led {
height: 3rem;
position:absolute;
position: absolute;
top: 0;
left: 0;
width: 100%;
@@ -129,4 +228,24 @@ button:active {
.ground-data {
padding: 0.1rem 0;
font-size: 0.8rem;
}
}
.extra {
height: 2rem;
display: flex;
flex-flow: row nowrap;
align-items: stretch;
justify-content: center;
}
.extra-data {
width: 1rem;
height: 1rem;
border-radius: 1rem;
margin: 0.5rem;
}
.extra-data-0 {
background: #000;
}
.extra-data-1 {
background: #aaa;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

+10 -11
View File
@@ -1,18 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Svelte app</title>
<title>Svelte app</title>
<link rel='icon' type='image/png' href='/favicon.png'>
<link rel='stylesheet' href='/global.css'>
<link rel='stylesheet' href='/build/bundle.css'>
<link rel="icon" type="image/png" href="/favicon.png" />
<link rel="stylesheet" href="/global.css" />
<link rel="stylesheet" href="/build/bundle.css" />
<script defer src='/build/bundle.js'></script>
</head>
<script defer src="/build/bundle.js"></script>
</head>
<body>
</body>
<body></body>
</html>
+373
View File
@@ -0,0 +1,373 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
max-width: 1200px;
margin: 40px auto;
padding: 0 10px;
font: 18px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
color: #444;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
h1,
h2,
h3 {
line-height: 1.2;
}
@media (prefers-color-scheme: dark) {
body {
color: #ccc;
background: black;
}
a:link {
color: #5bf;
}
a:visited {
color: #ccf;
}
}
.layout {
display: grid;
width: 36rem;
height: 13rem;
margin-bottom: 2rem;
gap: 0.5rem;
grid: 1.5rem 8.5rem 2rem / 32rem 3.5rem;
grid-template-areas:
"t a"
"b a"
"e a";
}
.name {
display: flex;
flex: row nowrap;
align-items: center;
justify-content: flex-start;
grid-area: t;
}
.ground {
grid-area: b;
display: grid;
width: 100%;
height: 100%;
gap: 0.25rem;
font-size: 1.5rem;
font-family: monospace;
}
.ground div {
display: flex;
align-items: center;
justify-content: center;
background-color: #ff04;
}
.ground.ground-32 {
grid: repeat(2, 1fr) / repeat(16, 1fr);
}
.ground.ground-16 {
grid: repeat(1, 1fr) / repeat(16, 1fr);
}
.ground.ground-8 {
grid: repeat(1, 1fr) / repeat(8, 1fr);
}
.ground.ground-voltex {
grid: repeat(2, 1fr) / repeat(8, 1fr);
grid-template-areas:
"ll lr a b c d rl rr"
"ll lr fl fl fr fr rl rr";
}
.air {
grid-area: a;
display: grid;
width: 100%;
height: 100%;
grid: repeat(6, 1fr) / repeat(1, 1fr);
gap: 0.25rem;
font-family: monospace;
}
.air div {
display: flex;
align-items: center;
justify-content: center;
background-color: #0ff4;
}
.air.air-single {
grid: repeat(1, 1fr) / repeat(1, 1fr);
}
.air.air-voltex {
grid-template-areas:
"rr"
"rl"
"rl"
"lr"
"lr"
"ll";
}
.extra {
grid-area: e;
display: grid;
width: 100%;
height: 100%;
grid: repeat(1, 1fr) / repeat(3, 1fr);
gap: 0.25rem;
font-family: monospace;
}
.extra div {
display: flex;
align-items: center;
justify-content: center;
background-color: #f004;
}
</style>
<title>Slidershim Layouts</title>
</head>
<body>
<div class="layout">
<div class="name">Keyboard 32-zone, Tasoller Layout</div>
<div class="ground ground-32">
<div>1</div>
<div>Q</div>
<div>2</div>
<div>W</div>
<div>3</div>
<div>E</div>
<div>4</div>
<div>R</div>
<div>5</div>
<div>T</div>
<div>6</div>
<div>Y</div>
<div>7</div>
<div>U</div>
<div>8</div>
<div>I</div>
<div>A</div>
<div>Z</div>
<div>S</div>
<div>X</div>
<div>D</div>
<div>C</div>
<div>F</div>
<div>V</div>
<div>G</div>
<div>B</div>
<div>H</div>
<div>N</div>
<div>J</div>
<div>M</div>
<div>K</div>
<div>,</div>
</div>
<div class="air">
<div>]</div>
<div>[</div>
<div>;</div>
<div>.</div>
<div>'</div>
<div>/</div>
</div>
<div class="extra">
<div>[enter]</div>
<div>[space]</div>
<div>[esc]</div>
</div>
</div>
<div class="layout">
<div class="name">Keyboard 32-zone, Yuancon Layout</div>
<div class="ground ground-32">
<div>5</div>
<div>3</div>
<div>1</div>
<div>Y</div>
<div>W</div>
<div>U</div>
<div>S</div>
<div>Q</div>
<div>O</div>
<div>M</div>
<div>K</div>
<div>I</div>
<div>G</div>
<div>E</div>
<div>C</div>
<div>A</div>
<div>6</div>
<div>4</div>
<div>2</div>
<div>Z</div>
<div>X</div>
<div>V</div>
<div>T</div>
<div>R</div>
<div>P</div>
<div>N</div>
<div>L</div>
<div>J</div>
<div>H</div>
<div>F</div>
<div>D</div>
<div>B</div>
</div>
<div class="air">
<div>;</div>
<div>\</div>
<div>]</div>
<div>[</div>
<div>=</div>
<div>-</div>
</div>
<div class="extra">
<div>[enter]</div>
<div>[space]</div>
<div>[esc]</div>
</div>
</div>
<body>
<div class="layout">
<div class="name">Keyboard 16-zone, Tasoller (Bottom Half) Layout</div>
<div class="ground ground-16">
<div>A</div>
<div>Z</div>
<div>S</div>
<div>X</div>
<div>D</div>
<div>C</div>
<div>F</div>
<div>V</div>
<div>G</div>
<div>B</div>
<div>H</div>
<div>N</div>
<div>J</div>
<div>M</div>
<div>K</div>
<div>,</div>
</div>
<div class="air">
<div>]</div>
<div>[</div>
<div>;</div>
<div>.</div>
<div>'</div>
<div>/</div>
</div>
<div class="extra">
<div>[enter]</div>
<div>[space]</div>
<div>[esc]</div>
</div>
</div>
<div class="layout">
<div class="name">Keyboard 8-zone, Deemo Layout</div>
<div class="ground ground-8">
<div>A</div>
<div>S</div>
<div>D</div>
<div>F</div>
<div>J</div>
<div>K</div>
<div>L</div>
<div>;</div>
</div>
<div class="air air-single">
<div>
<span style="writing-mode: vertical-lr">[space]</span>
</div>
</div>
<div class="extra">
<div>[disabled]</div>
<div>[disabled]</div>
<div>[disabled]</div>
</div>
</div>
<div class="layout">
<div class="name">Keyboard 10-zone, Voltex/Neardayo Layout</div>
<div class="ground ground-voltex">
<div style="grid-area: ll">W</div>
<div style="grid-area: lr">E</div>
<div style="grid-area: rl">O</div>
<div style="grid-area: rr">P</div>
<div style="grid-area: a">D</div>
<div style="grid-area: b">F</div>
<div style="grid-area: c">J</div>
<div style="grid-area: d">K</div>
<div style="grid-area: fl">C</div>
<div style="grid-area: fr">M</div>
</div>
<div class="air air-voltex">
<div style="grid-area: rr">P</div>
<div style="grid-area: rl">O</div>
<div style="grid-area: lr">E</div>
<div style="grid-area: ll">W</div>
</div>
<div class="extra">
<div>1</div>
<div>[esc]</div>
<div>[enter]</div>
</div>
</div>
<div class="layout">
<div class="name">XBOX 360 Gamepad, Voltex/Neardayo Layout</div>
<div class="ground ground-voltex">
<div style="grid-area: ll">LS<br />&lt;&lt;</div>
<div style="grid-area: lr">LS<br />&gt;&gt;</div>
<div style="grid-area: rl">RS<br />&lt;&lt;</div>
<div style="grid-area: rr">RS<br />&gt;&gt;</div>
<div style="grid-area: a">A</div>
<div style="grid-area: b">B</div>
<div style="grid-area: c">X</div>
<div style="grid-area: d">Y</div>
<div style="grid-area: fl">LB</div>
<div style="grid-area: fr">RB</div>
</div>
<div class="air air-voltex">
<div style="grid-area: rr">RS&gt;</div>
<div style="grid-area: rl">RS&lt;</div>
<div style="grid-area: lr">LS&gt;</div>
<div style="grid-area: ll">LS&lt;</div>
</div>
<div class="extra">
<div>[start]</div>
<div>[back]</div>
<div>[guide]</div>
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

+64
View File
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
max-width: 650px;
margin: 40px auto;
padding: 0 10px;
font: 18px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
color: #444;
}
h1,
h2,
h3 {
line-height: 1.2;
}
@media (prefers-color-scheme: dark) {
body {
color: #ccc;
background: black;
}
a:link {
color: #5bf;
}
a:visited {
color: #ccf;
}
}
</style>
<title>brokenithm-qr</title>
</head>
<body>
<h1>slidershim brokenithm helper</h1>
<ul class="links"></ul>
<script>
(function () {
const search = window.location.search;
if (search.slice(0, 3) !== "?d=" || search.length <= 3) {
return;
}
const ul = document.querySelector(".links");
search
.slice(3)
.split(";")
.map((x) => atob(x))
.forEach((ip) => {
const li = document.createElement("li");
const a = document.createElement("a");
a.innerText = ip;
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener");
a.href = `http://${ip}:1606/`;
li.appendChild(a);
ul.appendChild(li);
});
})();
</script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
/target/
+1720
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
[package]
name = "slider_io"
version = "0.1.0"
edition = "2021"
[dependencies]
# logging
log = "0.4.14"
env_logger = "0.9.0"
# threads
parking_lot = "0.12.0"
atomic_float = "0.1.0"
# async
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"
# UI
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
directories = "4.0.1"
image = "0.23.14"
# device and system
rusb = "0.9.0"
serialport = "4.0.1"
wwserial = {path = "../src-wwserial" }
vigem-client = "0.1.1"
winapi = "0.3.9"
ipconfig = "0.3.0"
# webserver
hyper = { version="0.14.16", features= ["server", "http1", "http2", "tcp", "stream", "runtime"] }
phf = { version = "0.10.1", features = ["macros"] }
tungstenite = { version="0.16.0", default-features=false }
tokio-tungstenite = "0.16.1"
# webserver utils
base64 = "0.13.0"
palette = "0.6.0"
qrcode = { version="0.12.0", features= ["image"] }
+15
View File
@@ -0,0 +1,15 @@
max_width = 100
hard_tabs = false
tab_spaces = 2
newline_style = "Auto"
use_small_heuristics = "Default"
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
edition = "2018"
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
imports_granularity = "Crate"
wrap_comments = true
+46
View File
@@ -0,0 +1,46 @@
extern crate slider_io;
use async_trait::async_trait;
use std::{future::Future, io, time::Duration};
use tokio::{select, time::sleep};
use slider_io::shared::worker::{AsyncHaltableJob, AsyncHaltableWorker};
struct CounterJob;
#[async_trait]
impl AsyncHaltableJob for CounterJob {
async fn run<F: Future<Output = ()> + 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(500)).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 => {},
}
}
}
#[tokio::main]
async fn main() {
env_logger::Builder::new()
.filter_level(log::LevelFilter::Debug)
.init();
let _worker = AsyncHaltableWorker::new("counter", CounterJob);
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
}
+20
View File
@@ -0,0 +1,20 @@
extern crate slider_io;
use std::io;
use slider_io::{
device::brokenithm::BrokenithmJob, shared::worker::AsyncHaltableWorker, state::SliderState,
};
#[tokio::main]
async fn main() {
env_logger::Builder::new()
.filter_level(log::LevelFilter::Debug)
.init();
let state = SliderState::new();
let _worker = AsyncHaltableWorker::new("brokenithm", BrokenithmJob::new(&state, &false, &false));
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
}
+35
View File
@@ -0,0 +1,35 @@
extern crate slider_io;
use std::{
io,
time::{Duration, Instant},
};
use slider_io::{device::diva, shared::worker::ThreadJob, state::SliderState};
fn main() {
env_logger::Builder::new()
.filter_level(log::LevelFilter::Debug)
.init();
let state = SliderState::new();
let mut job = diva::DivaSliderJob::new(&state, &"COM1".to_string(), 0x3f);
let ok = job.setup();
while ok {
job.tick();
}
// let state = SliderState::new();
// let timer = LoopTimer::new();
// let _worker = ThreadWorker::new(
// "d",
// diva::DivaSliderJob::new(&state, &"COM4".to_string(), 0x3f),
// timer,
// );
println!("Press enter to quit");
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
}
+47
View File
@@ -0,0 +1,47 @@
use std::{mem, ptr};
use winapi::um::{commapi::*, fileapi::*, minwinbase::*, synchapi::*, winbase::*, winnt::*};
fn main() {
unsafe {
let mut port: Vec<u16> = vec![];
port.extend("COM4".encode_utf16());
port.push(0);
let handle = CreateFileW(
port.as_ptr(),
GENERIC_READ | GENERIC_WRITE,
0,
ptr::null_mut(),
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
0 as HANDLE,
);
let mut overlapped_read: OVERLAPPED = mem::zeroed();
overlapped_read.hEvent = CreateEventW(ptr::null_mut(), 1, 0, ptr::null_mut());
let mut overlapped_write: OVERLAPPED = mem::zeroed();
overlapped_write.hEvent = CreateEventW(ptr::null_mut(), 0, 0, ptr::null_mut());
SetupComm(handle, 4096, 4096);
let mut timeouts: COMMTIMEOUTS = mem::zeroed();
GetCommTimeouts(handle, &mut timeouts);
SetCommMask(handle, 0x80); // EV_ERR
let mut dcb: DCB = mem::zeroed();
GetCommState(handle, &mut dcb);
dcb.BaudRate = 115200;
dcb.ByteSize = 8;
dcb.Parity = NOPARITY;
dcb.set_fParity(0);
dcb.StopBits = ONESTOPBIT;
dcb.set_fBinary(1);
PurgeComm(
handle,
PURGE_TXCLEAR | PURGE_TXABORT | PURGE_RXCLEAR | PURGE_RXABORT,
);
}
}
+102
View File
@@ -0,0 +1,102 @@
extern crate slider_io;
use std::{thread::sleep, time::Duration};
fn main() {
let mut sp = serialport::new("COM4", 115200).open().unwrap();
sp.write_request_to_send(true).unwrap();
sp.write_data_terminal_ready(true).unwrap();
sleep(Duration::from_millis(100));
println!("tx");
let res = sp.write(&[0xff, 0x01, 0x00, 0xf1]).unwrap();
println!("tx {}", res);
loop {
sleep(Duration::from_millis(100));
println!("rx");
let mut buf = [0u8; 4];
let res = sp.read(&mut buf).unwrap();
println!("rx {} {:?}", res, buf);
}
}
// use serial2::SerialPort;
// fn main() {
// let sp = SerialPort::open("COM4", 115200).unwrap();
// println!("Tx");
// let res = sp.write("data".as_bytes()).unwrap();
// println!("Tx {}", res);
// loop {
// println!("Rx");
// let mut buf = [0u8; 5];
// let res = sp.read(&mut buf).unwrap();
// // let res = sp.read(&mut buf).unwrap_or_else(|e| {
// // println!("Err {}", e);
// // 0
// // });
// println!("Rx {} {:?}", res, buf);
// }
// let mut sp = serialport::new("COM4", 115200).open().unwrap();
// println!("Tx");
// let res = sp.write("data".as_bytes()).unwrap();
// println!("Tx {}", res);
// loop {
// println!("Rx");
// let mut buf = [0u8; 5];
// let res = sp.read(&mut buf).unwrap();
// println!("Rx {} {:?}", res, buf);
// }
// }
// use serialport::SerialPort;
// use std::time::Duration;
// use tokio::{
// io::{self, AsyncReadExt, AsyncWriteExt},
// time::sleep,
// };
// use tokio_serial::SerialPortBuilderExt;
// #[tokio::main]
// async fn main() -> io::Result<()> {
// let mut sp = tokio_serial::new("COM4", 115200)
// .open_native_async()
// .unwrap();
// sp.write_request_to_send(true).unwrap();
// sp.write_data_terminal_ready(true).unwrap();
// // sp.set_timeout(Duration::from_millis(1000))?;
// let (mut rx_port, mut tx_port) = tokio::io::split(sp);
// // tx_port.write(&[41, 42, 43, 44]).await?;
// // let mut buf = [0u8; 10];
// // let res = rx_port.read(&mut buf).await?;
// // println!("{}, {:?}", res, buf);
// loop {
// let mut buf = [0u8; 10];
// let res = rx_port.read(&mut buf).await?;
// println!("{}, {:?}", res, buf);
// }
// // let mut serial_reader =
// // tokio_util::codec::FramedRead::new(rx_port,
// // tokio_util::codec::BytesCodec::new()); let serial_sink =
// // tokio_util::codec::FramedWrite::new(tx_port,
// // tokio_util::codec::BytesCodec::new());
// // println!("Tx");
// // let res = tx.write("data".as_bytes()).await?;
// // println!("Sent {}", res);
// // sleep(Duration::from_millis(1000)).await;
// // loop {
// // let mut buffer = [0u8; 1];
// // println!("Rx");
// // let res = rx.read(&mut buffer).await;
// // println!("{:?} {:?}", res, buffer);
// // }
// Ok(())
// }
+11
View File
@@ -0,0 +1,11 @@
extern crate slider_io;
use serialport::available_ports;
use std::io;
fn main() {
let res = available_ports();
println!("{:?}", res);
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
}
+73
View File
@@ -0,0 +1,73 @@
extern crate slider_io;
use std::io;
use slider_io::config::Config;
fn main() {
env_logger::Builder::new()
.filter_level(log::LevelFilter::Debug)
.init();
// voltex?
let config = Config::from_str(
r#"{
"deviceMode": "yuancon",
"outputMode": "gamepad-voltex",
"outputPolling": "60",
"keyboardSensitivity": 50,
"ledMode": "reactive-voltex",
"ledSensitivity": 50
}"#,
)
.unwrap();
println!("{:?}", config);
// serial?
let config = Config::from_str(
r#"{
"deviceMode": "yuancon",
"outputMode": "kb-32-tasoller",
"outputPolling": "60",
"keyboardSensitivity": 50,
"ledMode": "serial",
"ledSerialPort": "COM5"
}"#,
)
.unwrap();
println!("{:?}", config);
// basic
let config = Config::from_str(
r#"{
"deviceMode": "yuancon",
"outputMode": "kb-32-tasoller",
"keyboardSensitivity": 50,
"outputPolling": "60",
"ledMode": "reactive-8",
"ledSensitivity": 50
}"#,
)
.unwrap();
println!("{:?}", config);
// tasoller/
let config = Config::from_str(
r#"{
"deviceMode": "tasoller-two",
"outputMode": "kb-32-tasoller",
"outputPolling": "60",
"keyboardSensitivity": 50,
"ledMode": "reactive-8",
"ledSensitivity": 50
}"#,
)
.unwrap();
println!("{:?}", config);
// let manager = Context::new(config);
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
}
+41
View File
@@ -0,0 +1,41 @@
extern crate slider_io;
use std::{io, thread::sleep, time::Duration};
use slider_io::shared::{
utils::LoopTimer,
worker::{ThreadJob, ThreadWorker},
};
struct TestJob {
data: i64,
}
impl ThreadJob for TestJob {
fn setup(&mut self) -> bool {
self.data = 0;
println!("setup {}", self.data);
true
}
fn tick(&mut self) -> bool {
self.data += 1;
println!("tick {}", self.data);
sleep(Duration::from_millis(500));
true
}
}
impl Drop for TestJob {
fn drop(&mut self) {
self.data = -1;
println!("teardown {}", self.data);
}
}
fn main() {
let timer = LoopTimer::new();
let _worker = ThreadWorker::new("j", TestJob { data: 1 }, timer);
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
}
+75
View File
@@ -0,0 +1,75 @@
use log::{info, warn};
use serde_json::Value;
use std::fs;
use crate::{
device::config::DeviceMode, lighting::config::LightsMode, output::config::OutputMode, system,
};
#[derive(Debug, Clone)]
pub struct Config {
pub raw: String,
pub device_mode: DeviceMode,
pub output_mode: OutputMode,
pub lights_mode: LightsMode,
}
impl Config {
pub fn from_str(s: &str) -> Option<Config> {
let v: Value = serde_json::from_str(s).ok()?;
Some(Config {
raw: s.to_string(),
device_mode: DeviceMode::from_serde_value(&v)?,
output_mode: OutputMode::from_serde_value(&v)?,
lights_mode: LightsMode::from_serde_value(&v)?,
})
}
fn default() -> Self {
Self::from_str(
r#"{
"deviceMode": "none",
"devicePolling": "100",
"outputMode": "none",
"ledMode": "none",
"keyboardSensitivity": 20,
"outputWebsocketUrl": "localhost:3000",
"outputPolling": "100",
"ledSensitivity": 20,
"ledWebsocketUrl": "localhost:3001",
"ledSerialPort": "COM5"
}"#,
)
.unwrap()
}
fn load_saved() -> Option<Self> {
let config_path = system::get_config_path()?;
if !config_path.exists() {
return None;
}
info!("Config file found at {:?}", config_path);
let saved_data = fs::read_to_string(config_path.as_path()).ok()?;
return Self::from_str(saved_data.as_str());
}
pub fn load() -> Self {
Self::load_saved()
.or_else(|| {
warn!("Config loading from file failed, using default");
Some(Self::default())
})
.unwrap()
}
pub fn save(&self) -> Option<()> {
info!("Config saving...");
let config_path = system::get_config_path()?;
info!("Config saving to {:?}", config_path);
fs::write(config_path.as_path(), self.raw.as_str()).unwrap();
info!("Config saved");
Some(())
}
}
+129
View File
@@ -0,0 +1,129 @@
use atomic_float::AtomicF64;
use log::info;
use std::sync::{atomic::Ordering, Arc};
use crate::{
config::Config,
device::{brokenithm::BrokenithmJob, config::DeviceMode, diva::DivaSliderJob, hid::HidJob},
lighting::{config::LightsMode, lighting::LightsJob},
output::{config::OutputMode, output::OutputJob},
shared::{
utils::LoopTimer,
worker::{AsyncHaltableWorker, AsyncWorker, ThreadWorker},
},
state::SliderState,
};
#[allow(dead_code)]
pub struct Context {
state: SliderState,
config: Config,
device_thread_worker: Option<ThreadWorker>,
device_async_worker: Option<AsyncWorker>,
device_async_haltable_worker: Option<AsyncHaltableWorker>,
output_worker: Option<AsyncWorker>,
lights_worker: Option<AsyncWorker>,
timers: Vec<(&'static str, Arc<AtomicF64>)>,
}
impl Context {
pub fn new(config: Config) -> Self {
info!("Context creating");
info!("Device config {:?}", config.device_mode);
info!("Output config {:?}", config.output_mode);
info!("Lights config {:?}", config.lights_mode);
let state = SliderState::new();
let mut timers = vec![];
let (device_thread_worker, device_async_worker, device_async_haltable_worker) =
match &config.device_mode {
DeviceMode::None => (None, None, None),
DeviceMode::Brokenithm {
ground_only,
lights_enabled,
} => (
None,
None,
Some(AsyncHaltableWorker::new(
"brokenithm",
BrokenithmJob::new(&state, ground_only, lights_enabled),
)),
),
DeviceMode::Hardware { spec } => (
{
let timer = LoopTimer::new();
timers.push(("d", timer.fork()));
Some(ThreadWorker::new(
"device",
HidJob::from_config(&state, spec),
timer,
))
},
None,
None,
),
DeviceMode::DivaSlider { port, brightness } => (
{
let timer = LoopTimer::new();
timers.push(("d", timer.fork()));
Some(ThreadWorker::new(
"diva",
DivaSliderJob::new(&state, port, *brightness),
timer,
))
},
None,
None,
),
};
let output_worker = match &config.output_mode {
OutputMode::None => None,
_ => {
let timer = LoopTimer::new();
timers.push(("o", timer.fork()));
Some(AsyncWorker::new(
"output",
OutputJob::new(&state, &config.output_mode),
timer,
))
}
};
let lights_worker = match &config.lights_mode {
LightsMode::None => None,
_ => {
let timer = LoopTimer::new();
timers.push(("l", timer.fork()));
Some(AsyncWorker::new(
"lights",
LightsJob::new(&state, &config.lights_mode),
timer,
))
}
};
Self {
state,
config,
device_thread_worker,
device_async_worker,
device_async_haltable_worker,
output_worker,
lights_worker,
timers,
}
}
pub fn clone_state(&self) -> SliderState {
self.state.clone()
}
pub fn timer_state(&self) -> String {
self
.timers
.iter()
.map(|(s, f)| format!("{}:{:.1}/s", s, f.load(Ordering::SeqCst)))
.collect::<Vec<String>>()
.join(" ")
}
}
File diff suppressed because one or more lines are too long
@@ -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

@@ -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>
@@ -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>
@@ -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
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);
}
}
}
+62
View File
@@ -0,0 +1,62 @@
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,
brightness: u8,
},
}
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(),
brightness: u8::try_from(v["divaBrightness"].as_i64()?).ok()?,
},
"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,
})
}
}
+369
View File
@@ -0,0 +1,369 @@
use log::{error, info, warn};
use std::{
collections::VecDeque,
time::{Duration, Instant},
}; // thread::sleep, time::Duration
use wwserial::WwSerial;
use crate::{shared::worker::ThreadJob, state::SliderState};
/*
Init packet
0xff 0x10 0x00 0xf1
Report of all touch sliders at 16 pressure
0xff 0x01 0x20 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0xe0
Report of all touch sliders at 0-31 pressure
0xff 0x01 0x20 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 0x1c 0x1d 0x1e 0x1f 0x0f
*/
#[derive(Debug)]
struct DivaPacket {
command: u8,
len: u8,
data: Vec<u8>,
checksum: u8,
raw: Option<Vec<u8>>,
}
impl DivaPacket {
fn new() -> Self {
Self {
command: 0,
len: 0,
data: Vec::with_capacity(256),
checksum: 0,
raw: None,
}
}
fn from_bytes(command: u8, data: &[u8]) -> Self {
let checksum = 0xffu64
+ (command as u64)
+ (data.len() as u64)
+ data.iter().map(|x| (*x) as u64).sum::<u64>();
let checksum = ((0x100 - (checksum & 0xff)) & 0xff) as u8;
Self {
command,
len: data.len() as u8,
data: data.iter().copied().collect(),
checksum: checksum,
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) -> &Vec<u8> {
let mut raw: Vec<u8> = Vec::with_capacity(512);
raw.push(0xff);
Self::push_raw_escaped(self.command, &mut raw);
Self::push_raw_escaped(self.len, &mut raw);
for i in &self.data {
Self::push_raw_escaped(*i, &mut raw);
}
Self::push_raw_escaped(self.checksum, &mut raw);
// null pad?
// raw.push(0);
// debug!("Diva serializing {}", raw.len());
self.raw = Some(raw);
// debug!("Diva serializing {:?}", &self.raw);
return self.raw.as_ref().unwrap();
}
}
enum DivaDeserializerState {
ExpectCommand,
ExpectLen,
ExpectData,
ExpectChecksum,
Done,
}
struct DivaDeserializer {
state: DivaDeserializerState,
checksum: u64,
escape: u8,
len: u8,
packet: DivaPacket,
}
impl DivaDeserializer {
fn new() -> Self {
Self {
state: DivaDeserializerState::Done,
checksum: 0,
escape: 0,
len: 0,
packet: DivaPacket::new(),
}
}
fn deserialize(&mut self, data: &[u8], out: &mut VecDeque<DivaPacket>) {
// debug!("Diva deserializing {} {:?}", data.len(), data);
for c in data {
match c {
0xff => {
self.packet = DivaPacket::new();
self.checksum = 0xff;
self.state = DivaDeserializerState::ExpectCommand;
self.escape = 0;
// println!("{} open", c);
}
0xfd => {
self.escape = 1;
// println!("esc {}", c);
}
c => {
let c = c + self.escape;
self.escape = 0;
self.checksum += c as u64;
match self.state {
DivaDeserializerState::ExpectCommand => {
self.packet.command = c;
self.state = DivaDeserializerState::ExpectLen;
// println!("cmd {}", c);
}
DivaDeserializerState::ExpectLen => {
self.len = c;
self.packet.len = c;
self.state = match c {
0 => DivaDeserializerState::ExpectChecksum,
_ => DivaDeserializerState::ExpectData,
};
// println!("len {}", c);
}
DivaDeserializerState::ExpectData => {
self.packet.data.push(c);
self.len -= 1;
if self.len == 0 {
self.state = DivaDeserializerState::ExpectChecksum;
}
// println!("data {}", c);
}
DivaDeserializerState::ExpectChecksum => {
self.packet.checksum = c;
debug_assert!(self.checksum & 0xff == 0);
// println!("Packet complete {} {}", self.checksum, c);
if self.checksum & 0xff == 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,
ReadLoop,
}
pub struct DivaSliderJob {
state: SliderState,
port: String,
brightness: u8,
read_buf: Vec<u8>,
in_packets: VecDeque<DivaPacket>,
out_packets: VecDeque<DivaPacket>,
deserializer: DivaDeserializer,
serial_port: Option<WwSerial>,
bootstrap: DivaSliderBootstrap,
last_lights: Instant,
}
impl DivaSliderJob {
pub fn new(state: &SliderState, port: &String, brightness: u8) -> Self {
Self {
state: state.clone(),
port: port.clone(),
brightness,
read_buf: Vec::with_capacity(1024),
in_packets: VecDeque::with_capacity(100),
out_packets: VecDeque::with_capacity(100),
deserializer: DivaDeserializer::new(),
serial_port: None,
bootstrap: DivaSliderBootstrap::Init,
last_lights: Instant::now(),
}
}
}
impl ThreadJob for DivaSliderJob {
fn setup(&mut self) -> bool {
info!(
"Serial port for diva slider opening at {} {:?}",
self.port.as_str(),
115200
);
let serial_port = WwSerial::new(self.port.clone(), 115200, 5, 0, false);
if !serial_port.check() {
error!("Cannot open serial port at {}", self.port.as_str());
return false;
}
self.serial_port = Some(serial_port);
true
}
fn tick(&mut self) -> bool {
let mut work = false;
let serial_port = self.serial_port.as_mut().unwrap();
self.read_buf.clear();
let read_amount = serial_port.read(&mut self.read_buf) as usize;
if read_amount > 0 {
// debug!("Serial read {} bytes", read_amount);
self
.deserializer
.deserialize(&self.read_buf[0..read_amount], &mut self.in_packets);
}
match self.bootstrap {
DivaSliderBootstrap::Init => {
info!("Diva sending init");
let reset_packet = DivaPacket::from_bytes(0x10, &[]);
self.out_packets.push_back(reset_packet);
self.bootstrap = DivaSliderBootstrap::AwaitReset;
}
DivaSliderBootstrap::AwaitReset => {
while let Some(ack_packet) = self.in_packets.pop_front() {
if ack_packet.command == 0x10 && ack_packet.len == 0x00 && ack_packet.checksum == 0xf1 {
info!(
"Diva ack init {:#4x} {:?}",
ack_packet.command, ack_packet.data
);
info!("Diva sending info");
let info_packet = DivaPacket::from_bytes(0xf0, &[]);
self.out_packets.push_back(info_packet);
self.bootstrap = DivaSliderBootstrap::AwaitInfo;
break;
} else {
warn!(
"Unexpected packet {:#4x} {:?}",
ack_packet.command, ack_packet.data
);
}
}
}
DivaSliderBootstrap::AwaitInfo => {
if let Some(ack_packet) = self.in_packets.pop_front() {
info!(
"Diva ack info {:#4x} {:?}",
ack_packet.command, ack_packet.data
);
info!("Diva sending start");
let start_packet = DivaPacket::from_bytes(0x03, &[]);
self.out_packets.push_back(start_packet);
self.bootstrap = DivaSliderBootstrap::ReadLoop;
self.last_lights = Instant::now();
}
}
DivaSliderBootstrap::ReadLoop => {
while let Some(data_packet) = self.in_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]);
input_handle.flip_all();
work = true;
}
}
let mut send_lights = false;
let mut lights_buf = [0; 94];
{
let mut lights_handle = self.state.lights.lock();
// Send leds at least once a second to keep alive
if lights_handle.dirty || self.last_lights.elapsed() > Duration::from_millis(1000) {
send_lights = true;
lights_buf[0] = self.brightness;
for (buf_chunk, state_chunk) in lights_buf[1..94]
.chunks_mut(3)
.take(31)
.zip(lights_handle.ground.chunks(3).rev())
{
buf_chunk[0] = state_chunk[1];
buf_chunk[1] = state_chunk[0];
buf_chunk[2] = state_chunk[2];
}
lights_handle.dirty = false;
}
}
if send_lights {
self.last_lights = Instant::now();
let lights_packet = DivaPacket::from_bytes(0x02, &lights_buf);
self.out_packets.push_back(lights_packet);
}
}
};
// sleep(Duration::from_millis(3));
while let Some(mut packet) = self.out_packets.pop_front() {
work = true;
let data = packet.serialize();
let bytes_written = serial_port.write(data);
if bytes_written == 0 {
warn!("Serial write timeout");
}
// debug!("Serial write {}/{}", bytes_written, data.len());
}
// sleep(Duration::from_millis(3));
// TODO: async worker?
// sleep(Duration::from_millis(10));
work
}
}
impl Drop for DivaSliderJob {
fn drop(&mut self) {
match self.bootstrap {
DivaSliderBootstrap::ReadLoop => {
let serial_port = self.serial_port.as_mut().unwrap();
let mut stop_packet = DivaPacket::from_bytes(0x04, &[]);
serial_port.write(stop_packet.serialize());
}
_ => {}
};
info!("Diva serial port closed");
}
}
@@ -1,48 +1,33 @@
use log::{error, info};
use rusb::{self, DeviceHandle, GlobalContext};
use std::{
error,
error::Error,
mem::swap,
ops::{Deref, DerefMut},
thread,
time::Duration,
};
use log::{error, info};
use rusb::{self, DeviceHandle, GlobalContext};
use crate::slider_io::{
config::DeviceMode,
controller_state::{ControllerState, FullState, LedState},
worker::Job,
use crate::{
shared::{
utils::{Buffer, ShimError},
worker::ThreadJob,
},
state::{SliderInput, SliderLights, SliderState},
};
pub struct Buffer {
pub data: [u8; 256],
pub len: usize,
}
use super::config::HardwareSpec;
impl Buffer {
pub fn new() -> Self {
Buffer {
data: [0; 256],
len: 0,
}
}
fn slice(&self) -> &[u8] {
&self.data[0..self.len]
}
}
type HidReadCallback = fn(&Buffer, &mut ControllerState) -> ();
type HidLedCallback = fn(&mut Buffer, &LedState) -> ();
type HidReadCallback = fn(&Buffer, &mut SliderInput) -> ();
type HidLedCallback = fn(&mut Buffer, &SliderLights) -> ();
enum WriteType {
Bulk,
Interrupt,
}
pub struct HidDeviceJob {
state: FullState,
pub struct HidJob {
state: SliderState,
vid: u16,
pid: u16,
read_endpoint: u8,
@@ -50,6 +35,7 @@ pub struct HidDeviceJob {
read_callback: HidReadCallback,
read_buf: Buffer,
last_read_buf: Buffer,
led_write_type: WriteType,
led_callback: HidLedCallback,
@@ -58,9 +44,9 @@ pub struct HidDeviceJob {
handle: Option<DeviceHandle<GlobalContext>>,
}
impl HidDeviceJob {
impl HidJob {
fn new(
state: FullState,
state: SliderState,
vid: u16,
pid: u16,
read_endpoint: u8,
@@ -77,6 +63,7 @@ impl HidDeviceJob {
led_endpoint,
read_callback,
read_buf: Buffer::new(),
last_read_buf: Buffer::new(),
led_write_type: led_type,
led_callback,
led_buf: Buffer::new(),
@@ -84,15 +71,15 @@ impl HidDeviceJob {
}
}
pub fn from_config(state: &FullState, mode: &DeviceMode) -> Self {
match mode {
DeviceMode::TasollerOne => Self::new(
pub fn from_config(state: &SliderState, spec: &HardwareSpec) -> Self {
match spec {
HardwareSpec::TasollerOne => Self::new(
state.clone(),
0x1ccf,
0x2333,
0x84,
0x03,
|buf, controller_state| {
|buf, input| {
if buf.len != 11 {
return;
}
@@ -103,14 +90,15 @@ impl HidDeviceJob {
.flat_map(|x| (0..8).map(move |i| ((x) >> i) & 1))
.collect();
for i in 0..32 {
controller_state.ground_state[i] = bits[34 + i] * 255;
input.ground[i] = bits[34 + i] * 255;
}
input.flip_vert();
controller_state.air_state.copy_from_slice(&bits[28..34]);
controller_state.extra_state.copy_from_slice(&bits[26..28]);
input.air.copy_from_slice(&bits[28..34]);
input.extra[0..2].copy_from_slice(&bits[26..28]);
},
WriteType::Bulk,
|buf, led_state| {
|buf, lights| {
buf.len = 240;
buf.data[0] = 'B' as u8;
buf.data[1] = 'L' as u8;
@@ -118,36 +106,35 @@ impl HidDeviceJob {
for (buf_chunk, state_chunk) in buf.data[3..96]
.chunks_mut(3)
.take(31)
.zip(led_state.led_state.chunks(3).rev())
.zip(lights.ground.chunks(3).rev())
{
buf_chunk[0] = state_chunk[2];
buf_chunk[1] = state_chunk[1];
buf_chunk[2] = state_chunk[0];
buf_chunk[0] = state_chunk[1];
buf_chunk[1] = state_chunk[0];
buf_chunk[2] = state_chunk[2];
}
buf.data[96..240].fill(0);
},
),
DeviceMode::TasollerTwo => Self::new(
HardwareSpec::TasollerTwo => Self::new(
state.clone(),
0x1ccf,
0x2333,
0x84,
0x03,
|buf, controller_state| {
|buf, input| {
if buf.len != 36 {
return;
}
controller_state
.ground_state
.copy_from_slice(&buf.data[4..36]);
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();
controller_state.air_state.copy_from_slice(&bits[2..8]);
controller_state.extra_state.copy_from_slice(&bits[2..8]);
input.air.copy_from_slice(&bits[0..6]);
input.extra[0..2].copy_from_slice(&bits[6..8]);
},
WriteType::Bulk,
|buf, led_state| {
|buf, lights| {
buf.len = 240;
buf.data[0] = 'B' as u8;
buf.data[1] = 'L' as u8;
@@ -155,59 +142,57 @@ impl HidDeviceJob {
for (buf_chunk, state_chunk) in buf.data[3..96]
.chunks_mut(3)
.take(31)
.zip(led_state.led_state.chunks(3).rev())
.zip(lights.ground.chunks(3).rev())
{
buf_chunk[0] = state_chunk[2];
buf_chunk[1] = state_chunk[1];
buf_chunk[2] = state_chunk[0];
buf_chunk[0] = state_chunk[1];
buf_chunk[1] = state_chunk[0];
buf_chunk[2] = state_chunk[2];
}
buf.data[96..240].fill(0);
},
),
DeviceMode::Yuancon => Self::new(
HardwareSpec::Yuancon => Self::new(
state.clone(),
0x1973,
0x2001,
0x81,
0x02,
|buf, controller_state| {
|buf, input| {
if buf.len != 34 {
return;
}
controller_state
.ground_state
.copy_from_slice(&buf.data[2..34]);
input.ground.copy_from_slice(&buf.data[2..34]);
for i in 0..6 {
controller_state.air_state[i ^ 1] = if buf.data[0] & (1 << i) == 0 { 1 } else { 0 };
input.air[i ^ 1] = (buf.data[0] >> i) & 1;
}
for i in 0..3 {
controller_state.extra_state[i] = if buf.data[1] & (1 << i) == 0 { 1 } else { 0 };
input.extra[2 - i] = (buf.data[1] >> i) & 1;
}
},
WriteType::Interrupt,
|buf, led_state| {
|buf, lights| {
buf.len = 31 * 2;
for (buf_chunk, state_chunk) in buf
.data
.chunks_mut(2)
.take(31)
.zip(led_state.led_state.chunks(3).rev())
.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);
}
},
),
_ => panic!("Not implemented"),
}
}
fn setup_impl(&mut self) -> Result<(), Box<dyn error::Error>> {
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!("Could not find device");
error!("Device not found");
return Err(Box::new(ShimError));
}
let mut handle = handle.unwrap();
info!("Device found {:?}", handle);
@@ -227,55 +212,81 @@ impl HidDeviceJob {
const TIMEOUT: Duration = Duration::from_millis(20);
impl Job for HidDeviceJob {
fn setup(&mut self) {
self.setup_impl().unwrap();
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) {
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;
if self.read_buf.len != 0 {
let mut controller_state_handle = self.state.controller_state.lock().unwrap();
(self.read_callback)(&self.read_buf, controller_state_handle.deref_mut());
// 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 led_state_handle = self.state.led_state.lock().unwrap();
if led_state_handle.dirty {
(self.led_callback)(&mut self.led_buf, led_state_handle.deref());
led_state_handle.dirty = false;
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.data, TIMEOUT),
WriteType::Bulk => handle.write_bulk(self.led_endpoint, self.led_buf.slice(), TIMEOUT),
WriteType::Interrupt => {
handle.write_interrupt(self.led_endpoint, &self.led_buf.data, TIMEOUT)
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;
}
}
}
// thread::sleep(Duration::from_millis(10));
work
}
}
fn teardown(&mut self) {
let handle = self.handle.as_mut().unwrap();
handle.release_interface(0).ok();
impl Drop for HidJob {
fn drop(&mut self) {
if let Some(handle) = self.handle.as_mut() {
handle.release_interface(0).ok();
}
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod config;
pub mod brokenithm;
pub mod diva;
pub mod hid;
+23
View File
@@ -0,0 +1,23 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#![feature(div_duration)]
#![feature(more_qualified_paths)]
pub mod config;
pub mod shared;
pub mod state;
pub mod device;
pub mod lighting;
pub mod output;
pub mod system;
pub mod context;
pub mod manager;
pub use config::Config;
pub use manager::Manager;
pub use system::{get_brokenithm_qr_path, get_log_file_path, list_ips};
+57
View File
@@ -0,0 +1,57 @@
use serde_json::Value;
#[derive(Debug, Clone, Copy)]
pub enum ReactiveLayout {
Even { splits: usize },
Voltex,
}
#[derive(Debug, Clone)]
pub enum LightsMode {
None,
Reactive {
layout: ReactiveLayout,
sensitivity: u8,
},
Attract,
Test,
Websocket {
url: String,
},
Serial {
port: String,
},
}
impl LightsMode {
pub fn from_serde_value(v: &Value) -> Option<Self> {
Some(match v["ledMode"].as_str()? {
"none" => LightsMode::None,
"reactive-4" => LightsMode::Reactive {
layout: ReactiveLayout::Even { splits: 4 },
sensitivity: u8::try_from(v["ledSensitivity"].as_i64()?).ok()?,
},
"reactive-8" => LightsMode::Reactive {
layout: ReactiveLayout::Even { splits: 8 },
sensitivity: u8::try_from(v["ledSensitivity"].as_i64()?).ok()?,
},
"reactive-16" => LightsMode::Reactive {
layout: ReactiveLayout::Even { splits: 16 },
sensitivity: u8::try_from(v["ledSensitivity"].as_i64()?).ok()?,
},
"reactive-voltex" => LightsMode::Reactive {
layout: ReactiveLayout::Voltex,
sensitivity: u8::try_from(v["ledSensitivity"].as_i64()?).ok()?,
},
"attract" => LightsMode::Attract,
"test" => LightsMode::Test,
"websocket" => LightsMode::Websocket {
url: v["ledWebsocketUrl"].as_str()?.to_string(),
},
"serial" => LightsMode::Serial {
port: v["ledSerialPort"].as_str()?.to_string(),
},
_ => return None,
})
}
}
+231
View File
@@ -0,0 +1,231 @@
use async_trait::async_trait;
use log::{error, info};
use palette::{FromColor, Hsv, Srgb};
use serialport::{ClearBuffer, SerialPort};
use std::{
ops::DerefMut,
time::{Duration, Instant},
};
use tokio::time::{interval, Interval};
use crate::{
shared::{utils::Buffer, voltex::VoltexState, worker::AsyncJob},
state::{SliderLights, SliderState},
};
use super::config::{LightsMode, ReactiveLayout};
pub struct LightsJob {
state: SliderState,
mode: LightsMode,
serial_port: Option<Box<dyn SerialPort>>,
started: Instant,
timer: Interval,
}
impl LightsJob {
pub fn new(state: &SliderState, mode: &LightsMode) -> Self {
Self {
state: state.clone(),
mode: mode.clone(),
serial_port: None,
started: Instant::now(),
timer: interval(Duration::from_micros(33333)),
}
}
fn calc_lights(
&self,
flat_input: Option<&Vec<bool>>,
serial_buffer: Option<&Buffer>,
lights: &mut SliderLights,
) {
match self.mode {
LightsMode::Reactive { layout, .. } => {
let flat_input = flat_input.unwrap();
match layout {
ReactiveLayout::Even { splits } => {
let buttons_per_split = 32 / splits;
let banks: Vec<bool> = flat_input
.chunks(32 / splits)
.take(splits)
.map(|x| x.contains(&true))
.collect();
for idx in 0..31 {
lights.paint(
idx,
match (idx + 1) % buttons_per_split {
0 => &[255, 0, 255],
_ => match banks[idx / buttons_per_split] {
true => &[255, 0, 255],
false => &[255, 255, 0],
},
},
);
}
}
ReactiveLayout::Voltex => {
lights.ground.fill(0);
// Fixed
lights.paint(3, &[10, 100, 180]);
for idx in 0..5 {
lights.paint(7 + idx * 4, &[64, 64, 64]);
}
lights.paint(27, &[180, 10, 110]);
let voltex_input = VoltexState::from_flat(flat_input);
// Left laser
for (idx, state) in voltex_input.laser[0..2].iter().enumerate() {
if *state {
lights.paint(0 + idx * 4, &[70, 230, 250]);
lights.paint(1 + idx * 4, &[70, 230, 250]);
lights.paint(2 + idx * 4, &[70, 230, 250]);
}
}
// Right laser
for (idx, state) in voltex_input.laser[2..4].iter().enumerate() {
if *state {
lights.paint(24 + idx * 4, &[250, 60, 200]);
lights.paint(25 + idx * 4, &[255, 60, 200]);
lights.paint(26 + idx * 4, &[255, 60, 200]);
}
}
// Buttons
for (idx, state) in voltex_input.bt.iter().enumerate() {
if *state {
lights.paint(8 + idx * 4, &[255, 255, 255]);
lights.paint(10 + idx * 4, &[255, 255, 255]);
}
}
// Fx
for (idx, state) in voltex_input.fx.iter().enumerate() {
if *state {
lights.paint(9 + idx * 8, &[250, 100, 30]);
lights.paint(11 + idx * 8, &[250, 100, 30]);
lights.paint(13 + idx * 8, &[250, 100, 30]);
}
}
}
}
}
LightsMode::Attract => {
let theta = self
.started
.elapsed()
.div_duration_f64(Duration::from_secs(4))
% 1.0;
for idx in 0..31 {
let slice_theta = (&theta + (idx as f64) / 32.0) % 1.0;
let color = Srgb::from_color(Hsv::new(slice_theta * 360.0, 1.0, 1.0)).into_format::<u8>();
lights.paint(idx, &[color.red, color.green, color.blue]);
}
}
LightsMode::Serial { .. } => {
// https://github.com/jmontineri/OpeNITHM/blob/89e9a43f7484e8949cd31bbff79c32f21ea3ec1d/Firmware/OpeNITHM/SerialProcessor.h
// https://github.com/jmontineri/OpeNITHM/blob/89e9a43f7484e8949cd31bbff79c32f21ea3ec1d/Firmware/OpeNITHM/SerialProcessor.cpp
// https://github.com/jmontineri/OpeNITHM/blob/89e9a43f7484e8949cd31bbff79c32f21ea3ec1d/Firmware/OpeNITHM/SerialLeds.h
// https://github.com/jmontineri/OpeNITHM/blob/89e9a43f7484e8949cd31bbff79c32f21ea3ec1d/Firmware/OpeNITHM/SerialLeds.cpp
if let Some(serial_buffer) = serial_buffer {
// println!("buffer {:?}", serial_buffer.data);
if serial_buffer.data[0] == 0xaa && serial_buffer.data[1] == 0xaa {
for (idx, buf_chunk) in serial_buffer.data[2..95]
.chunks(3)
.take(31)
.rev()
.enumerate()
{
lights.paint(idx, &[(*buf_chunk)[1], (*buf_chunk)[2], (*buf_chunk)[0]]);
}
}
}
}
_ => panic!("Not implemented"),
}
lights.dirty = true;
}
}
#[async_trait]
impl AsyncJob for LightsJob {
async fn setup(&mut self) -> bool {
match &self.mode {
LightsMode::Serial { port } => {
info!(
"Serial port for led opening at {} {:?}",
port.as_str(),
115200
);
self.serial_port = match serialport::new(port, 115200).open() {
Ok(s) => {
info!("Serial port opened");
Some(s)
}
Err(e) => {
error!("Serial port could not open: {}", e);
None
}
};
self.serial_port.is_some()
}
_ => true,
}
}
async fn tick(&mut self) -> bool {
let mut flat_input: Option<Vec<bool>> = None;
let mut serial_buffer: Option<Buffer> = None;
// Do the IO here
match self.mode {
LightsMode::Reactive { sensitivity, .. } => {
let input_handle = self.state.input.lock();
flat_input = Some(input_handle.to_flat(&sensitivity));
}
LightsMode::Serial { .. } => {
if let Some(serial_port) = self.serial_port.as_mut() {
let mut serial_data_avail = serial_port.bytes_to_read().unwrap_or(0);
if serial_data_avail >= 100 {
if serial_data_avail % 100 == 0 {
let mut serial_buffer_working = Buffer::new();
serial_port
.as_mut()
.read_exact(&mut serial_buffer_working.data[..100])
.ok()
.unwrap();
serial_data_avail -= 100;
serial_buffer = Some(serial_buffer_working);
}
if serial_data_avail > 0 {
serial_port.clear(ClearBuffer::All).unwrap();
}
}
}
}
_ => {}
}
// Then calculate and transfer
{
let mut lights_handle = self.state.lights.lock();
self.calc_lights(
flat_input.as_ref(),
serial_buffer.as_ref(),
lights_handle.deref_mut(),
);
}
self.timer.tick().await;
true
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod config;
pub mod lighting;
+107
View File
@@ -0,0 +1,107 @@
use log::info;
use parking_lot::Mutex;
use std::{
sync::Arc,
thread::{self, JoinHandle},
};
use tokio::{
select,
sync::{mpsc, oneshot},
};
use crate::{config::Config, context::Context, state::SliderState};
pub struct Manager {
state: Arc<Mutex<Option<SliderState>>>,
context: Arc<Mutex<Option<Context>>>,
join_handle: Option<JoinHandle<()>>,
tx_config: mpsc::UnboundedSender<Config>,
tx_stop: Option<oneshot::Sender<()>>,
}
impl Manager {
pub fn new() -> Self {
let state = Arc::new(Mutex::new(None));
let (tx_config, mut rx_config) = mpsc::unbounded_channel::<Config>();
let (tx_stop, rx_stop) = oneshot::channel::<()>();
let context: Arc<Mutex<Option<Context>>> = Arc::new(Mutex::new(None));
let state_cloned = Arc::clone(&state);
let context_cloned = Arc::clone(&context);
let join_handle = thread::spawn(move || {
info!("Manager thread started");
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4)
.enable_all()
.build()
.unwrap();
runtime.block_on(async move {
info!("Manager runtime started");
select! {
_ = async {
loop {
match rx_config.recv().await {
Some(config) => {
info!("Rebuilding context");
let mut context_handle = context_cloned.lock();
context_handle.take();
let new_context = Context::new(config);
let new_state = new_context.clone_state();
context_handle.replace(new_context);
let mut state_handle = state_cloned.lock();
state_handle.replace(new_state);
},
None => {
let mut context_handle = context_cloned.lock();
context_handle.take();
}
}
}
} => {},
_ = rx_stop => {}
}
});
});
Self {
state,
context,
join_handle: Some(join_handle),
tx_config,
tx_stop: Some(tx_stop),
}
}
pub fn update_config(&self, config: Config) {
self.tx_config.send(config).unwrap();
}
pub fn try_get_state(&self) -> Option<SliderState> {
let state_handle = self.state.lock();
state_handle.as_ref().map(|x| x.clone())
}
pub fn get_timer_state(&self) -> String {
let context_handle = self.context.lock();
context_handle
.as_ref()
.map(|context| context.timer_state())
.unwrap_or("".to_string())
}
}
impl Drop for Manager {
fn drop(&mut self) {
if let Some(tx_stop) = self.tx_stop.take() {
tx_stop.send(()).ok();
}
if let Some(join_handle) = self.join_handle.take() {
join_handle.join().ok();
}
}
}
+121
View File
@@ -0,0 +1,121 @@
use serde_json::Value;
#[derive(Debug, Clone, Copy)]
pub enum PollingRate {
Sixty,
Hundred,
TwoHundredFifty,
FiveHundred,
Thousand,
}
#[derive(Debug, Clone, Copy)]
pub enum KeyboardLayout {
Tasoller,
Yuancon,
TasollerHalf,
Deemo,
Voltex,
Neardayo,
}
#[derive(Debug, Clone, Copy)]
pub enum GamepadLayout {
Voltex,
Neardayo,
}
#[derive(Debug, Clone)]
pub enum OutputMode {
None,
Keyboard {
layout: KeyboardLayout,
polling: PollingRate,
sensitivity: u8,
},
Gamepad {
layout: GamepadLayout,
polling: PollingRate,
sensitivity: u8,
},
Websocket {
url: String,
polling: PollingRate,
},
}
impl PollingRate {
pub fn from_str(s: &str) -> Option<Self> {
match s {
"60" => Some(PollingRate::Sixty),
"100" => Some(PollingRate::Hundred),
"250" => Some(PollingRate::TwoHundredFifty),
"500" => Some(PollingRate::FiveHundred),
"1000" => Some(PollingRate::Thousand),
_ => None,
}
}
pub fn to_t_u64(&self) -> u64 {
match self {
PollingRate::Sixty => 16666,
PollingRate::Hundred => 10000,
PollingRate::TwoHundredFifty => 4000,
PollingRate::FiveHundred => 2000,
PollingRate::Thousand => 1000,
}
}
}
impl OutputMode {
pub fn from_serde_value(v: &Value) -> Option<Self> {
Some(match v["outputMode"].as_str().unwrap() {
"none" => OutputMode::None,
"kb-32-tasoller" => OutputMode::Keyboard {
layout: KeyboardLayout::Tasoller,
polling: PollingRate::from_str(v["outputPolling"].as_str()?)?,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"kb-32-yuancon" => OutputMode::Keyboard {
layout: KeyboardLayout::Yuancon,
polling: PollingRate::from_str(v["outputPolling"].as_str()?)?,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"kb-16-tasoller" => OutputMode::Keyboard {
layout: KeyboardLayout::TasollerHalf,
polling: PollingRate::from_str(v["outputPolling"].as_str()?)?,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"kb-8-deemo" => OutputMode::Keyboard {
layout: KeyboardLayout::Deemo,
polling: PollingRate::from_str(v["outputPolling"].as_str()?)?,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"kb-voltex" => OutputMode::Keyboard {
layout: KeyboardLayout::Voltex,
polling: PollingRate::from_str(v["outputPolling"].as_str()?)?,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"kb-neardayo" => OutputMode::Keyboard {
layout: KeyboardLayout::Neardayo,
polling: PollingRate::from_str(v["outputPolling"].as_str()?)?,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"gamepad-voltex" => OutputMode::Gamepad {
layout: GamepadLayout::Voltex,
polling: PollingRate::from_str(v["outputPolling"].as_str()?)?,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"gamepad-neardayo" => OutputMode::Gamepad {
layout: GamepadLayout::Neardayo,
polling: PollingRate::from_str(v["outputPolling"].as_str()?)?,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"websocket" => OutputMode::Websocket {
url: v["outputWebsocketUrl"].as_str()?.to_string(),
polling: PollingRate::from_str(v["outputPolling"].as_str()?)?,
},
_ => return None,
})
}
}
+173
View File
@@ -0,0 +1,173 @@
use log::error;
use std::error::Error;
use vigem_client::{Client, TargetId, XButtons, XGamepad, Xbox360Wired};
use crate::shared::voltex::VoltexState;
use super::{config::GamepadLayout, output::OutputHandler};
struct LastWind {
left: bool,
right: bool,
out: i16,
}
impl LastWind {
fn new() -> Self {
LastWind {
left: false,
right: false,
out: 0,
}
}
fn update(&mut self, left: bool, right: bool) -> i16 {
let out = match (left, right) {
(false, false) => 0,
(true, false) => -1,
(false, true) => 1,
(true, true) => match (self.left, self.right) {
(false, false) => 0,
(true, false) => 1,
(false, true) => -1,
(true, true) => self.out,
},
};
self.left = left;
self.right = right;
self.out = out;
out
}
}
pub struct GamepadOutput {
target: Xbox360Wired<Client>,
use_air: bool,
gamepad: XGamepad,
left_wind: LastWind,
right_wind: LastWind,
}
impl GamepadOutput {
pub fn new(layout: GamepadLayout) -> Option<Self> {
let target = Self::get_target();
let use_air = match layout {
GamepadLayout::Neardayo => true,
_ => false,
};
match target {
Ok(target) => Some(Self {
target,
use_air,
gamepad: XGamepad::default(),
left_wind: LastWind::new(),
right_wind: LastWind::new(),
}),
Err(e) => {
error!("Gamepad connection error: {}", e);
error!("Gamepad connection error: Is ViGEMBus missing?");
None
}
}
}
fn get_target() -> Result<Xbox360Wired<Client>, Box<dyn Error>> {
let client = Client::connect()?;
let mut target = Xbox360Wired::new(client, TargetId::XBOX360_WIRED);
target.plugin()?;
target.wait_ready()?;
Ok(target)
}
fn update(&mut self) -> bool {
match self.target.update(&self.gamepad) {
Ok(_) => true,
Err(e) => {
error!("Gamepad update error: {}", e);
false
}
}
}
}
impl OutputHandler for GamepadOutput {
fn tick(&mut self, flat_input: &Vec<bool>) -> bool {
let voltex_state = VoltexState::from_flat(flat_input);
let buttons = voltex_state
.bt
.iter()
.chain(voltex_state.fx.iter())
.chain(voltex_state.extra.iter())
.zip([
XButtons::A,
XButtons::B,
XButtons::X,
XButtons::Y,
XButtons::LB,
XButtons::RB,
XButtons::START,
XButtons::BACK,
XButtons::GUIDE,
])
.fold(0, |buttons, (state, code)| {
buttons
| match state {
true => code,
false => 0,
}
});
let lx = self.left_wind.update(
voltex_state.laser[0] || (self.use_air && flat_input[32]),
voltex_state.laser[1] || (self.use_air && (flat_input[33] || flat_input[34])),
) * 20000;
let rx = self.right_wind.update(
voltex_state.laser[2] || (self.use_air && (flat_input[35] || flat_input[36])),
voltex_state.laser[3] || (self.use_air && flat_input[37]),
) * 20000;
let mut dirty = false;
if self.gamepad.buttons.raw != buttons {
self.gamepad.buttons.raw = buttons;
dirty = true;
}
if self.gamepad.thumb_lx != lx {
self.gamepad.thumb_lx = lx;
dirty = true;
}
if self.gamepad.thumb_rx != rx {
self.gamepad.thumb_rx = rx;
dirty = true;
}
match dirty {
true => self.update(),
false => true,
}
}
fn reset(&mut self) {
self.gamepad = XGamepad::default();
self.update();
}
}
impl Drop for GamepadOutput {
fn drop(&mut self) {
match self.target.unplug() {
Ok(_) => {}
Err(e) => {
error!("Gamepad unplug error: {}", e);
}
}
}
}
// dammit vigem_client::Event
unsafe impl Send for GamepadOutput {}
@@ -1,11 +1,13 @@
use std::mem;
use winapi::{
ctypes::c_int,
um::winuser::{SendInput, INPUT, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP},
};
use crate::slider_io::config::KeyboardLayout;
use super::{
config::KeyboardLayout,
output::OutputHandler
};
#[rustfmt::skip]
const TASOLLER_KB_MAP: [usize; 41] = [
@@ -29,23 +31,71 @@ const YUANCON_KB_MAP: [usize; 41] = [
0x0d, 0x20, 0x1b, // VK_RETURN, VK_SPACE, VK_ESCAPE
];
#[rustfmt::skip]
const TASOLLER_HALF_KB_MAP: [usize; 41] = [
0x41, 0x41 /* A */, 0x5a, 0x5a /* Z */, 0x53, 0x53 /* S */, 0x58, 0x58 /* X */,
0x44, 0x44 /* D */, 0x43, 0x43 /* C */, 0x46, 0x46 /* F */, 0x56, 0x56 /* V */,
0x47, 0x47 /* G */, 0x42, 0x42 /* B */, 0x48, 0x48 /* H */, 0x4e, 0x4e /* N */,
0x4a, 0x4a /* J */, 0x4d, 0x4d /* M */, 0x4b, 0x4b /* K */, 0xbc, 0xbc /* VK_OEM_COMMA */,
0xbf, 0xde, 0xbe, // VK_OEM_2, VK_OEM_7, VK_OEM_PERIOD,
0xba, 0xdd, 0xdb, // VK_OEM_1, VK_OEM_6, VK_OEM_4
0x0d, 0x20, 0x1b // VK_RETURN, VK_SPACE, VK_ESCAPE
];
#[rustfmt::skip]
const DEEMO_KB_MAP: [usize; 41] = [
0x35, 0x35, 0x35, 0x35, 0x35, 0x35, // S
0x4f, 0x4f, 0x4f, 0x4f, // D
0x46, 0x46, 0x46, 0x46, 0x46, 0x46, // F
0x4a, 0x4a, 0x4a, 0x4a, 0x4a, 0x4a, // J
0x41, 0x41, 0x41, 0x41, // A
0x53, 0x53, 0x53, 0x53, // S
0x44, 0x44, 0x44, 0x44, // D
0x46, 0x46, 0x46, 0x46, // F
0x4a, 0x4a, 0x4a, 0x4a, // J
0x4b, 0x4b, 0x4b, 0x4b, // K
0x4c, 0x4c, 0x4c, 0x4c, 0x4c, 0x4c, // L
0x4c, 0x4c, 0x4c, 0x4c, // L
0xba, 0xba, 0xba, 0xba, // VK_OEM_1
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, // VK_SPACE
0x00, 0x00, 0x00, // Disabled
];
#[rustfmt::skip]
const VOLTEX_KB_MAP: [usize; 41] = [
0x57, 0x57, 0x57, 0x57, // W
0x45, 0x45, 0x45, 0x45, // E
0x43, 0x44,
0x43, 0x44,
0x43, 0x46, // D
0x43, 0x46, // C // F
0x4d, 0x4a, // M // J
0x4d, 0x4a, // K
0x4d, 0x4b,
0x4d, 0x4b,
0x4f, 0x4f, 0x4f, 0x4f, // O
0x50, 0x50, 0x50, 0x50, // P
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Disabled
0x31, 0x0d, 0x1b, // 1, VK_RETURN, VK_ESCAPE
];
#[rustfmt::skip]
const VOLTEX_KB_MAP_NEARDAYO: [usize; 41] = [
0x57, 0x57, 0x57, 0x57, // W
0x45, 0x45, 0x45, 0x45, // E
0x43, 0x44,
0x43, 0x44,
0x43, 0x46, // D
0x43, 0x46, // C // F
0x4d, 0x4a, // M // J
0x4d, 0x4a, // K
0x4d, 0x4b,
0x4d, 0x4b,
0x4f, 0x4f, 0x4f, 0x4f, // O
0x50, 0x50, 0x50, 0x50, // P
0x57, 0x45, 0x45, 0x4f, 0x4f, 0x50, // Disabled
0x31, 0x0d, 0x1b, // 1, VK_RETURN, VK_ESCAPE
];
pub struct KeyboardOutput {
ground_to_idx: [usize; 41],
idx_to_keycode: [u16; 41],
keycode_to_idx: [usize; 256],
// keycode_to_idx: [usize; 256],
next_keys: [bool; 41],
last_keys: [bool; 41],
@@ -58,7 +108,10 @@ impl KeyboardOutput {
let kb_map = match layout {
KeyboardLayout::Tasoller => &TASOLLER_KB_MAP,
KeyboardLayout::Yuancon => &YUANCON_KB_MAP,
KeyboardLayout::TasollerHalf => &TASOLLER_HALF_KB_MAP,
KeyboardLayout::Deemo => &DEEMO_KB_MAP,
KeyboardLayout::Voltex => &VOLTEX_KB_MAP,
KeyboardLayout::Neardayo => &VOLTEX_KB_MAP_NEARDAYO,
};
let mut ground_to_idx = [0 as usize; 41];
@@ -92,7 +145,7 @@ impl KeyboardOutput {
Self {
ground_to_idx,
idx_to_keycode,
keycode_to_idx,
// keycode_to_idx,
next_keys: [false; 41],
last_keys: [false; 41],
kb_buf,
@@ -100,23 +153,6 @@ impl KeyboardOutput {
}
}
pub fn tick(&mut self, flat_controller_state: &Vec<bool>) {
self
.next_keys
.iter_mut()
.zip(flat_controller_state)
.for_each(|(a, b)| {
*a = *b;
});
// println!("{:?}", self.next_keys);
self.send();
}
pub fn reset(&mut self) {
self.next_keys.fill(false);
self.send();
}
fn send(&mut self) {
self.n_kb_buf = 0;
@@ -126,22 +162,24 @@ impl KeyboardOutput {
.zip(self.last_keys.iter_mut())
.enumerate()
{
let wVk = self.idx_to_keycode[i];
if wVk == 0 {
let keycode = self.idx_to_keycode[i];
if keycode == 0 {
continue;
}
match (*n, *l) {
(false, true) => {
let inner: &mut KEYBDINPUT = unsafe { self.kb_buf[self.n_kb_buf as usize].u.ki_mut() };
inner.wVk = self.idx_to_keycode[i];
inner.dwFlags = 0;
self.n_kb_buf += 1;
}
(true, false) => {
let inner: &mut KEYBDINPUT = unsafe { self.kb_buf[self.n_kb_buf as usize].u.ki_mut() };
inner.wVk = self.idx_to_keycode[i];
inner.wVk = keycode;
inner.dwFlags = 0;
self.n_kb_buf += 1;
// println!("{} down", keycode);
}
(false, true) => {
let inner: &mut KEYBDINPUT = unsafe { self.kb_buf[self.n_kb_buf as usize].u.ki_mut() };
inner.wVk = keycode;
inner.dwFlags = KEYEVENTF_KEYUP;
self.n_kb_buf += 1;
// println!("{} up", keycode);
}
_ => {}
}
@@ -159,3 +197,27 @@ impl KeyboardOutput {
}
}
}
impl OutputHandler for KeyboardOutput {
fn tick(&mut self, flat_input: &Vec<bool>) -> bool {
self.next_keys.fill(false);
for (idx, x) in flat_input.iter().enumerate() {
if *x {
self.next_keys[self.ground_to_idx[idx]] = true;
}
}
self.send();
true
}
fn reset(&mut self) {
self.next_keys.fill(false);
self.send();
}
}
impl Drop for KeyboardOutput {
fn drop(&mut self) {
self.reset();
}
}
+6
View File
@@ -0,0 +1,6 @@
pub mod config;
mod gamepad;
mod keyboard;
pub mod output;
+95
View File
@@ -0,0 +1,95 @@
use async_trait::async_trait;
use log::error;
use std::time::Duration;
use tokio::time::{interval, Interval};
use crate::{shared::worker::AsyncJob, state::SliderState};
use super::{config::OutputMode, gamepad::GamepadOutput, keyboard::KeyboardOutput};
pub trait OutputHandler: Send {
fn tick(&mut self, flat_input: &Vec<bool>) -> bool;
fn reset(&mut self);
}
pub struct OutputJob {
state: SliderState,
mode: OutputMode,
sensitivity: u8,
handler: Option<Box<dyn OutputHandler>>,
timer: Interval,
}
impl OutputJob {
pub fn new(state: &SliderState, mode: &OutputMode) -> Self {
Self {
state: state.clone(),
mode: mode.clone(),
sensitivity: 0,
handler: None,
timer: interval(Duration::MAX),
}
}
}
#[async_trait]
impl AsyncJob for OutputJob {
async fn setup(&mut self) -> bool {
match self.mode {
OutputMode::Keyboard {
layout,
polling,
sensitivity,
} => {
self.sensitivity = sensitivity;
self.handler = Some(Box::new(KeyboardOutput::new(layout.clone())));
self.timer = interval(Duration::from_micros(polling.to_t_u64()));
true
}
OutputMode::Gamepad {
layout,
polling,
sensitivity,
} => {
self.sensitivity = sensitivity;
let handler = GamepadOutput::new(layout.clone());
self.timer = interval(Duration::from_micros(polling.to_t_u64()));
match handler {
Some(handler) => {
self.handler = Some(Box::new(handler));
true
}
None => false,
}
}
_ => {
error!("Not implemented");
false
}
}
}
async fn tick(&mut self) -> bool {
let flat_input = {
let input_handle = self.state.input.lock();
input_handle.to_flat(&self.sensitivity)
};
if let Some(handler) = self.handler.as_mut() {
handler.tick(&flat_input);
}
self.timer.tick().await;
true
}
}
impl Drop for OutputJob {
fn drop(&mut self) {
if let Some(handler) = self.handler.as_mut() {
handler.reset();
}
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod serial;
pub mod utils;
pub mod voltex;
pub mod worker;
+31
View File
@@ -0,0 +1,31 @@
use serialport::COMPort;
use std::{os::windows::prelude::AsRawHandle, time::Duration};
use winapi::{
shared::minwindef::DWORD,
um::{commapi::SetCommTimeouts, winbase::COMMTIMEOUTS},
};
pub trait ReadWriteTimeout {
fn set_read_write_timeout(&self, timeout: Duration) -> Result<(), ()>;
}
impl ReadWriteTimeout for COMPort {
fn set_read_write_timeout(&self, timeout: Duration) -> Result<(), ()> {
let milliseconds = timeout.as_secs() * 1000 + timeout.subsec_nanos() as u64 / 1_000_000;
let mut timeouts = COMMTIMEOUTS {
ReadIntervalTimeout: 0,
ReadTotalTimeoutMultiplier: 0,
ReadTotalTimeoutConstant: milliseconds as DWORD,
WriteTotalTimeoutMultiplier: 0,
WriteTotalTimeoutConstant: milliseconds as DWORD,
};
if unsafe { SetCommTimeouts(self.as_raw_handle(), &mut timeouts) } == 0 {
return Err(());
}
Ok(())
}
}
+85
View File
@@ -0,0 +1,85 @@
use atomic_float::AtomicF64;
use std::{
error::Error,
fmt,
sync::{atomic::Ordering, Arc},
time::{Duration, Instant},
};
pub struct Buffer {
pub data: [u8; 256],
pub len: usize,
}
#[allow(dead_code)]
impl Buffer {
pub fn new() -> Self {
Buffer {
data: [0; 256],
len: 0,
}
}
pub fn slice(&self) -> &[u8] {
&self.data[0..self.len]
}
}
#[derive(Debug)]
pub struct ShimError;
impl<'a> fmt::Display for ShimError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ShimError")
}
}
impl Error for ShimError {
fn description(&self) -> &str {
"shimError"
}
}
pub struct LoopTimer {
cap: usize,
cur: usize,
buf: Vec<Instant>,
freq: Arc<AtomicF64>,
}
impl LoopTimer {
pub fn new() -> Self {
Self {
cap: 100,
cur: 0,
buf: vec![Instant::now() - Duration::from_secs(10_000); 100],
freq: Arc::new(AtomicF64::new(0.0)),
}
}
pub fn tick(&mut self) {
let last = self.buf[self.cur];
let now = Instant::now();
self.buf[self.cur] = now;
let delta = (now - last) / 100 + Duration::from_micros(1);
let freq = Duration::from_millis(1000)
.div_duration_f64(delta)
.clamp(0.0, 9999.0);
self.freq.store(freq, Ordering::SeqCst);
self.cur = match self.cur + 1 {
cur if cur == self.cap => 0,
cur => cur,
}
}
#[allow(dead_code)]
pub fn reset(&mut self) {
self.buf = vec![Instant::now() - Duration::from_secs(10); 100];
self.cur = 0;
}
pub fn fork(&self) -> Arc<AtomicF64> {
Arc::clone(&self.freq)
}
}
+39
View File
@@ -0,0 +1,39 @@
pub struct VoltexState {
pub laser: [bool; 4],
pub bt: [bool; 4],
pub fx: [bool; 2],
pub extra: [bool; 3],
}
impl VoltexState {
pub fn from_flat(flat_input: &Vec<bool>) -> Self {
let mut voltex_state = Self {
laser: [false; 4],
bt: [false; 4],
fx: [false; 2],
extra: [false; 3],
};
voltex_state.laser[0] = flat_input[0..4].contains(&true);
voltex_state.laser[1] = flat_input[4..8].contains(&true);
voltex_state.laser[2] = flat_input[24..28].contains(&true);
voltex_state.laser[3] = flat_input[28..32].contains(&true);
for i in 0..4 {
voltex_state.bt[i] = flat_input[9 + i * 4] || flat_input[11 + i * 4];
}
for i in 0..2 {
voltex_state.fx[i] = flat_input[8 + i * 8]
|| flat_input[10 + i * 8]
|| flat_input[12 + i * 8]
|| flat_input[14 + i * 8];
}
for i in 0..3 {
voltex_state.extra[i] = flat_input[38 + i];
}
voltex_state
}
}
+171
View File
@@ -0,0 +1,171 @@
use async_trait::async_trait;
use log::info;
use std::{
future::Future,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
};
use tokio::{sync::oneshot, task};
use super::utils::LoopTimer;
pub trait ThreadJob: Send {
fn setup(&mut self) -> bool;
fn tick(&mut self) -> bool;
}
pub struct ThreadWorker {
name: &'static str,
thread: Option<thread::JoinHandle<()>>,
stop_signal: Arc<AtomicBool>,
}
impl ThreadWorker {
pub fn new<T: 'static + ThreadJob>(name: &'static str, mut job: T, mut timer: LoopTimer) -> Self {
info!("Thread worker starting {}", name);
let stop_signal = Arc::new(AtomicBool::new(false));
let stop_signal_clone = Arc::clone(&stop_signal);
Self {
name,
thread: Some(thread::spawn(move || {
let setup_res = job.setup();
stop_signal_clone.store(!setup_res, Ordering::SeqCst);
loop {
if stop_signal_clone.load(Ordering::SeqCst) {
break;
}
if job.tick() {
timer.tick();
}
}
info!("Thread worker received stop {}", name);
})),
stop_signal,
}
}
}
impl Drop for ThreadWorker {
fn drop(&mut self) {
info!("Thread worker stopping gracefully {}", self.name);
self.stop_signal.store(true, Ordering::SeqCst);
if let Some(thread) = self.thread.take() {
thread.join().ok();
};
info!("Thread worker stopped {}", self.name);
}
}
#[async_trait]
pub trait AsyncJob: Send + 'static {
async fn setup(&mut self) -> bool;
async fn tick(&mut self) -> bool;
}
pub struct AsyncWorker {
name: &'static str,
task: Option<task::JoinHandle<()>>,
stop_signal: Arc<AtomicBool>,
}
impl AsyncWorker {
pub fn new<T>(name: &'static str, mut job: T, mut timer: LoopTimer) -> Self
where
T: AsyncJob,
{
info!("Async worker starting {}", name);
let stop_signal = Arc::new(AtomicBool::new(false));
let stop_signal_clone = Arc::clone(&stop_signal);
let task = tokio::spawn(async move {
let setup_res = job.setup().await;
stop_signal_clone.store(!setup_res, Ordering::SeqCst);
loop {
if stop_signal_clone.load(Ordering::SeqCst) {
break;
}
if job.tick().await {
timer.tick();
}
}
info!("Async worker received stop {}", name);
});
Self {
name,
task: Some(task),
stop_signal,
}
}
}
impl Drop for AsyncWorker {
fn drop(&mut self) {
info!("Async worker stopping gracefully {}", self.name);
self.stop_signal.store(true, Ordering::SeqCst);
drop(self.task.take());
info!("Async worker stopped {}", self.name);
}
}
#[async_trait]
pub trait AsyncHaltableJob: Send + 'static {
async fn run<F: Future<Output = ()> + Send>(self, stop_signal: F);
}
pub struct AsyncHaltableWorker {
name: &'static str,
task: Option<task::JoinHandle<()>>,
stop_signal: Option<oneshot::Sender<()>>,
}
impl AsyncHaltableWorker {
pub fn new<T>(name: &'static str, job: T) -> Self
where
T: AsyncHaltableJob,
{
info!("AsyncHaltable worker starting {}", name);
let (send_stop, recv_stop) = oneshot::channel::<()>();
let task = tokio::spawn(async move {
job
.run(async move {
recv_stop.await.ok();
info!("AsyncHaltable worker received stop {}", name);
})
.await;
});
Self {
name,
task: Some(task),
stop_signal: Some(send_stop),
}
}
}
impl Drop for AsyncHaltableWorker {
fn drop(&mut self) {
info!("AsyncHaltable worker stopping gracefully {}", self.name);
if let Some(stop_signal) = self.stop_signal.take() {
stop_signal.send(()).ok();
}
self.task.take();
info!("AsyncHaltable worker stopped {}", self.name);
}
}
+133
View File
@@ -0,0 +1,133 @@
use parking_lot::Mutex;
use std::{sync::Arc, time::Instant};
/// Stores the input state of a slider controller, including ground touch pads,
/// air strings and extra buttons.
pub struct SliderInput {
/// Represents touch pressure in 32 touch pads in a 2 tall and 16 wide grid.
/// Each pressur is in a `u8` from 0 to 255. Pads are represented in order of
/// bottom left, then top left, then flows from left to right.
pub ground: [u8; 32],
/// Represents air string state in 6 pads starting from bottom to top. 0 means
/// uninterrupted, 1 means interrupted.
pub air: [u8; 6],
/// Represents extra button state, usually used for coin/test/card entry
/// functions.
pub extra: [u8; 3],
}
impl SliderInput {
/// Make a blank input state.
pub fn new() -> Self {
Self {
ground: [0; 32],
air: [0; 6],
extra: [0; 3],
}
}
/// Converts an input state to a `Vec<bool>`, used for output simulation and
/// visualisation.
pub fn to_flat(&self, sensitivity: &u8) -> Vec<bool> {
self
.ground
.iter()
.map(|x| x >= sensitivity)
.chain(self.air.iter().chain(self.extra.iter()).map(|x| x > &0))
.collect()
}
/// Flips the ground slider state vertically. Used when taking input for
/// tasoller controllers as they report starting from top left (instead of
/// botton left that is used internally).
pub fn flip_vert(&mut self) {
for i in 0..16 {
self.ground.swap(i * 2, i * 2 + 1);
}
}
pub fn flip_all(&mut self) {
for i in 0..16 {
self.ground.swap(i, 31 - i);
}
}
}
// Stores the lighting state of a slider controller.
pub struct SliderLights {
/// Represents the RGB pixel values of the slider controller from left to
/// right. Alternates between 16 touch pad pixels and 15 divider pixels.
pub ground: [u8; 3 * 31],
/// Internal dirty flag used to indicate that new lighting data is available.
pub dirty: bool,
/// To deprecate
pub start: Instant,
}
impl SliderLights {
/// Make a blank lighting state.
pub fn new() -> Self {
Self {
ground: [0; 3 * 31],
dirty: false,
start: Instant::now(),
}
}
/// Apply a RGB color to some pixel in the lighting state.
pub fn paint(&mut self, idx: usize, color: &[u8; 3]) {
self.ground[3 * idx..3 * (idx + 1)].copy_from_slice(color);
}
}
/// Stores data required for a single slider controller. Data and lighting
/// states are stored seperately in their own `Arc<Mutex<T>>` so that they can
/// be locked independently.
pub struct SliderState {
/// Input data for the slider controller.
pub input: Arc<Mutex<SliderInput>>,
/// Lighting data for the slider controller.
pub lights: Arc<Mutex<SliderLights>>,
}
impl SliderState {
/// Creates a blank slider controller state
pub fn new() -> Self {
Self {
input: Arc::new(Mutex::new(SliderInput::new())),
lights: Arc::new(Mutex::new(SliderLights::new())),
}
}
/// Takes an instantaneous slider controller state (input + lighting) as a
/// `Vec<u8>` that can be used for visualisation.
pub fn snapshot(&self) -> Vec<u8> {
let mut buf: Vec<u8> = vec![];
{
let input_handle = self.input.lock();
buf.extend(input_handle.ground);
buf.extend(input_handle.air);
buf.extend(input_handle.extra);
};
{
let lights_handle = self.lights.lock();
buf.extend(lights_handle.ground);
};
buf
}
}
impl Clone for SliderState {
fn clone(&self) -> Self {
Self {
input: Arc::clone(&self.input),
lights: Arc::clone(&self.lights),
}
}
}
+59
View File
@@ -0,0 +1,59 @@
use directories::ProjectDirs;
use image::Luma;
use qrcode::QrCode;
use std::{error::Error, fs, path::PathBuf};
pub fn list_ips() -> Result<Vec<String>, Box<dyn Error>> {
let mut ips = vec![];
for adapter in ipconfig::get_adapters()? {
for ip_address in adapter.ip_addresses() {
ips.push(format!("{}", ip_address));
}
}
Ok(ips)
}
/// Get the %APPDATA% path for config files (and create if it does not already
/// exist).
fn get_config_dir() -> Option<Box<PathBuf>> {
let project_dir = ProjectDirs::from("me", "impress labs", "slidershim").unwrap();
let config_dir = project_dir.config_dir();
fs::create_dir_all(config_dir).ok()?;
Some(Box::new(config_dir.to_path_buf()))
}
/// Generates a helper QR for connecting with brokenithm
pub fn get_brokenithm_qr_path() -> Option<Box<PathBuf>> {
let config_dir = get_config_dir()?;
let brokenithm_qr_path = config_dir.join("brokenithm.png");
let ips = list_ips().ok()?;
let link = "http://imp.ress.me/t/sshelper?d=".to_string()
+ &ips
.into_iter()
.filter(|s| s.as_str().chars().filter(|x| *x == '.').count() == 3)
.map(|s| base64::encode_config(s, base64::URL_SAFE_NO_PAD))
.collect::<Vec<String>>()
.join(";");
let qr = QrCode::new(link).ok()?;
let image = qr.render::<Luma<u8>>().build();
image.save(brokenithm_qr_path.as_path()).ok()?;
return Some(Box::new(brokenithm_qr_path));
}
pub fn get_log_file_path() -> Option<Box<PathBuf>> {
let config_dir = get_config_dir()?;
let log_path = config_dir.join("log.txt");
return Some(Box::new(log_path));
}
pub fn get_config_path() -> Option<Box<PathBuf>> {
let config_dir = get_config_dir()?;
let config_path = config_dir.join("config.json");
return Some(Box::new(config_path));
}
+634 -341
View File
File diff suppressed because it is too large Load Diff
+9 -10
View File
@@ -1,6 +1,6 @@
[package]
name = "slidershim"
version = "0.1.0"
version = "0.2.0"
description = "slidershim"
authors = ["4yn"]
license = ""
@@ -9,22 +9,21 @@ default-run = "slidershim"
edition = "2018"
build = "src/build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.0.0-beta.4" }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-beta.8", features = ["api-all", "system-tray"] }
rusb = "0.9.0"
palette = "0.6.0"
winapi = "0.3.9"
directories = "4.0.1"
log = "0.4.14"
simple-logging = "2.0.2"
env_logger = "0.9.0"
parking_lot = "0.12.0"
tauri = { version = "1.0.0-beta.8", features = ["shell-open", "system-tray"] }
open = "2.0.2"
slider_io = {path = "../src-slider_io" }
[features]
default = [ "custom-protocol" ]
custom-protocol = [ "tauri/custom-protocol" ]
+16
View File
@@ -0,0 +1,16 @@
_ _ _ _ _
___| (_) __| | ___ _ __ ___| |__ (_)_ __ ___
/ __| | |/ _` |/ _ \ '__/ __| '_ \| | '_ ` _ \
\__ \ | | (_| | __/ | \__ \ | | | | | | | | |
|___/_|_|\__,_|\___|_| |___/_| |_|_|_| |_| |_| v0.2.0
===============================================
https://github.com/4yn/slidershim
# Changelog
- v0.2.0
- Added support for Hori Project DIVA slider protocol
- Added 16 key keyboard layout
- v0.1.4
- Initial public release
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 582 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File
-37
View File
@@ -1,37 +0,0 @@
extern crate slidershim;
use std::io;
use slidershim::slider_io::{Config, Manager};
fn main() {
env_logger::Builder::new()
.filter_level(log::LevelFilter::Debug)
.init();
// let config = Config::from_str(
// r#"{
// "deviceMode": "yuancon",
// "outputMode": "kb-32-tasoller",
// "ledMode": "reactive-8",
// "keyboardSensitivity": 50
// }"#,
// )
// .unwrap();
let config = Config::from_str(
r#"{
"deviceMode": "tasoller-two",
"outputMode": "kb-32-tasoller",
"keyboardSensitivity": 50,
"ledMode": "reactive-8",
"ledSensitivity": 50
}"#,
)
.unwrap();
let manager = Manager::new(config);
let mut input = String::new();
let string = io::stdin().read_line(&mut input).unwrap();
}
-31
View File
@@ -1,31 +0,0 @@
extern crate slidershim;
use std::io;
// use slidershim::slider_io::worker::{Job, Worker};
// struct TestJob {
// data: i64,
// }
// impl Job for TestJob {
// fn setup(&mut self) {
// self.data = 10;
// println!("setup {}", self.data);
// }
// fn tick(&mut self) {
// self.data -= 1;
// println!("tick {}", self.data);
// }
// fn teardown(&mut self) {
// self.data = 11;
// println!("teardown {}", self.data);
// }
// }
fn main() {
// let worker = Worker::new(TestJob { data: 1 });
let mut input = String::new();
let string = io::stdin().read_line(&mut input).unwrap();
}
+47 -1
View File
@@ -1,3 +1,49 @@
use std::{
env, fs,
path::{Path, PathBuf},
};
const COPY_DIR: &'static str = "res";
fn copy_dir<P, Q>(from: P, to: Q)
where
P: AsRef<Path>,
Q: AsRef<Path>,
{
// https://stackoverflow.com/a/68950006
let to = to.as_ref().to_path_buf();
match fs::read_dir(from) {
Ok(paths) => {
for path in paths {
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 */
}
}
}
Err(_) => {}
}
}
fn main() {
tauri_build::build()
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();
}
-8
View File
@@ -1,8 +0,0 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
#![feature(div_duration)]
#![feature(more_qualified_paths)]
pub mod slider_io;
+95 -20
View File
@@ -5,11 +5,11 @@
#![feature(div_duration)]
#![feature(more_qualified_paths)]
mod slider_io;
use slider_io;
use std::sync::{Arc, Mutex};
use parking_lot::Mutex;
use std::sync::Arc;
use env_logger;
use log::info;
use tauri::{
@@ -17,10 +17,12 @@ use tauri::{
};
fn show_window<R: Runtime>(handle: &AppHandle<R>) {
handle.emit_all("ackShow", "").ok();
handle.get_window("main").unwrap().show().ok();
}
fn hide_window<R: Runtime>(handle: &AppHandle<R>) {
handle.emit_all("ackHide", "").ok();
handle.get_window("main").unwrap().hide().ok();
}
@@ -29,15 +31,32 @@ fn quit_app() {
}
fn main() {
env_logger::init();
// Setup logger
let config = Arc::new(Mutex::new(Some(slider_io::Config::default())));
#[cfg(debug_assertions)]
env_logger::Builder::new()
.filter_level(log::LevelFilter::Debug)
.init();
#[cfg(not(debug_assertions))]
{
config.lock().unwrap().as_ref().unwrap().save();
let log_file_path = slider_io::system::get_log_file_path().unwrap();
simple_logging::log_to_file(log_file_path.as_path(), log::LevelFilter::Debug).unwrap();
}
let config = Arc::new(Mutex::new(Some(slider_io::Config::load())));
let manager = Arc::new(Mutex::new(slider_io::Manager::new()));
{
let config_handle = config.lock();
let config_handle_ref = config_handle.as_ref().unwrap();
config_handle_ref.save();
let manager_handle = manager.lock();
manager_handle.update_config(config_handle_ref.clone());
}
tauri::Builder::default()
.system_tray(
// System tray content
SystemTray::new().with_menu(
SystemTrayMenu::new()
.add_item(CustomMenuItem::new("slidershim".to_string(), "slidershim").disabled())
@@ -46,6 +65,7 @@ fn main() {
),
)
.on_system_tray_event(|app_handle, event| match event {
// System tray events
SystemTrayEvent::LeftClick {
position: _,
size: _,
@@ -67,44 +87,99 @@ fn main() {
_ => {}
})
.setup(move |app| {
// Before app starts
// Hide event
let app_handle = app.handle();
app.listen_global("hide", move |_| {
hide_window(&app_handle);
});
// Quit event
app.listen_global("quit", |_| {
quit_app();
});
// Show logs
app.listen_global("openLogfile", |_| {
let log_file_path = slider_io::get_log_file_path();
if let Some(log_file_path) = log_file_path {
open::that(log_file_path.as_path()).ok();
}
});
// Show brokenithm qr
app.listen_global("openBrokenithmQr", |_| {
let brokenithm_qr_path = slider_io::get_brokenithm_qr_path();
if let Some(brokenithm_qr_path) = brokenithm_qr_path {
open::that(brokenithm_qr_path.as_path()).ok();
}
});
// UI ready event
let app_handle = app.handle();
let config_clone = Arc::clone(&config);
app.listen_global("heartbeat", move |e| {
let config_handle = config_clone.lock().unwrap();
info!("Heartbeat received");
app.listen_global("ready", move |_| {
let config_handle = config_clone.lock();
info!("Start signal received");
app_handle
.emit_all(
"showConfig",
Some(config_handle.as_ref().unwrap().raw.as_str().to_string()),
)
.unwrap();
let ips = slider_io::list_ips();
if let Ok(ips) = ips {
app_handle.emit_all("listIps", &ips).unwrap();
}
});
// UI update event
let app_handle = app.handle();
let manager_clone = Arc::clone(&manager);
app.listen_global("queryState", move |_| {
// app_handle.emit_all("showState", "@@@");
let (snapshot, timer) = {
let manager_handle = manager_clone.lock();
(
manager_handle.try_get_state().map(|x| x.snapshot()),
manager_handle.get_timer_state(),
)
};
match snapshot {
Some(snapshot) => {
app_handle.emit_all("showState", snapshot).ok();
}
_ => {}
}
app_handle.emit_all("showTimerState", timer).ok();
});
// Config set event
let config_clone = Arc::clone(&config);
let manager_clone = Arc::clone(&manager);
app.listen_global("setConfig", move |event| {
let payload = event.payload().unwrap();
info!("Config applied {}", payload);
if let Some(new_config) = slider_io::Config::from_str(payload) {
let mut config_handle = config_clone.lock().unwrap();
let mut config_handle = config_clone.lock();
config_handle.take();
config_handle.replace(new_config);
config_handle.as_ref().unwrap().save();
let config_handle_ref = config_handle.as_ref().unwrap();
config_handle_ref.save();
let manager_handle = manager_clone.lock();
manager_handle.update_config(config_handle_ref.clone());
}
});
let app_handle = app.handle();
app.listen_global("hide", move |_| {
hide_window(&app_handle);
});
app.listen_global("quit", |_| {
quit_app();
});
Ok(())
})
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(|app_handle, event| match event {
// After app starts
Event::CloseRequested { label, api, .. } if label.as_str() == "main" => {
api.prevent_close();
hide_window(app_handle);
-173
View File
@@ -1,173 +0,0 @@
use std::{convert::TryFrom, fs, path::PathBuf};
use log::info;
use directories::ProjectDirs;
use serde_json::Value;
#[derive(Debug, Clone)]
pub enum DeviceMode {
None,
TasollerOne,
TasollerTwo,
Yuancon,
Brokenithm { ground_only: bool },
}
#[derive(Debug, Clone, Copy)]
pub enum KeyboardLayout {
Tasoller,
Yuancon,
Deemo,
}
#[derive(Debug, Clone)]
pub enum OutputMode {
None,
Keyboard {
layout: KeyboardLayout,
sensitivity: u8,
},
Websocket {
url: String,
},
}
#[derive(Debug, Clone, Copy)]
pub enum ReactiveLayout {
Four,
Eight,
Sixteen,
}
#[derive(Debug, Clone)]
pub enum LedMode {
None,
Reactive {
layout: ReactiveLayout,
sensitivity: u8,
},
Attract,
Test,
Websocket {
url: String,
},
}
#[derive(Debug, Clone)]
pub struct Config {
pub raw: String,
pub device_mode: DeviceMode,
pub output_mode: OutputMode,
pub led_mode: LedMode,
}
impl Config {
pub fn from_str(s: &str) -> Option<Config> {
let v: Value = serde_json::from_str(s).ok()?;
Some(Config {
raw: s.to_string(),
device_mode: match v["deviceMode"].as_str()? {
"none" => DeviceMode::None,
"tasoller-one" => DeviceMode::TasollerOne,
"tasoller-two" => DeviceMode::TasollerTwo,
"yuancon" => DeviceMode::Yuancon,
"brokenithm" => DeviceMode::Brokenithm { ground_only: false },
"brokenithm-ground" => DeviceMode::Brokenithm { ground_only: true },
_ => panic!("Invalid device mode"),
},
output_mode: match v["outputMode"].as_str().unwrap() {
"none" => OutputMode::None,
"kb-32-tasoller" => OutputMode::Keyboard {
layout: KeyboardLayout::Tasoller,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"kb-32-yuancon" => OutputMode::Keyboard {
layout: KeyboardLayout::Yuancon,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"kb-6-deemo" => OutputMode::Keyboard {
layout: KeyboardLayout::Deemo,
sensitivity: u8::try_from(v["keyboardSensitivity"].as_i64()?).ok()?,
},
"websocket" => OutputMode::Websocket {
url: v["outputWebsocketUrl"].to_string(),
},
_ => panic!("Invalid output mode"),
},
led_mode: match v["ledMode"].as_str()? {
"none" => LedMode::None,
"reactive-4" => LedMode::Reactive {
layout: ReactiveLayout::Four,
sensitivity: u8::try_from(v["ledSensitivity"].as_i64()?).ok()?,
},
"reactive-8" => LedMode::Reactive {
layout: ReactiveLayout::Eight,
sensitivity: u8::try_from(v["ledSensitivity"].as_i64()?).ok()?,
},
"reactive-16" => LedMode::Reactive {
layout: ReactiveLayout::Sixteen,
sensitivity: u8::try_from(v["ledSensitivity"].as_i64()?).ok()?,
},
"attract" => LedMode::Attract,
"test" => LedMode::Test,
"websocket" => LedMode::Websocket {
url: v["ledWebsocketUrl"].to_string(),
},
_ => panic!("Invalid led mode"),
},
})
}
fn factory() -> Self {
Self::from_str(
r#"{
"deviceMode": "none",
"outputMode": "none",
"ledMode": "none",
"keyboardSensitivity": 20,
"outputWebsocketUrl": "localhost:3000",
"ledSensitivity": 20,
"ledWebsocketUrl": "localhost:3001"
}"#,
)
.unwrap()
}
fn get_saved_path() -> Option<Box<PathBuf>> {
let project_dir = ProjectDirs::from("me", "imp.ress", "slidershim").unwrap();
let config_dir = project_dir.config_dir();
fs::create_dir_all(config_dir);
let config_path = config_dir.join("config.json");
return Some(Box::new(config_path));
}
fn load_saved() -> Option<Self> {
let config_path = Self::get_saved_path()?;
if !config_path.exists() {
return None;
}
info!("Config file found at {:?}", config_path);
let mut saved_data = fs::read_to_string(config_path.as_path()).ok()?;
return Self::from_str(saved_data.as_str());
}
pub fn default() -> Self {
Self::load_saved()
.or_else(|| Some(Self::factory()))
.unwrap()
}
pub fn save(&self) -> Option<()> {
info!("Config saving...");
let config_path = Self::get_saved_path()?;
info!("Config saving to {:?}", config_path);
fs::write(config_path.as_path(), self.raw.as_str()).unwrap();
info!("Config saved");
Some(())
}
}
@@ -1,82 +0,0 @@
use std::{
sync::{Arc, Mutex},
time::Instant,
};
pub struct ControllerState {
pub ground_state: [u8; 32],
pub air_state: [u8; 6],
pub extra_state: [u8; 3],
}
impl ControllerState {
pub fn new() -> Self {
Self {
ground_state: [0; 32],
air_state: [0; 6],
extra_state: [0; 3],
}
}
pub fn flat(&self, sensitivity: &u8) -> Vec<bool> {
self
.ground_state
.iter()
.map(|x| x > sensitivity)
.chain(
self
.air_state
.iter()
.chain(self.extra_state.iter())
.map(|x| x > &0),
)
.collect()
}
}
pub struct LedState {
pub led_state: [u8; 3 * 31],
pub dirty: bool,
pub start: Instant,
}
impl LedState {
pub fn new() -> Self {
Self {
led_state: [0; 3 * 31],
dirty: false,
start: Instant::now(),
}
}
}
pub struct FullState {
pub controller_state: Arc<Mutex<ControllerState>>,
pub led_state: Arc<Mutex<LedState>>,
}
impl FullState {
pub fn new() -> Self {
Self {
controller_state: Arc::new(Mutex::new(ControllerState::new())),
led_state: Arc::new(Mutex::new(LedState::new())),
}
}
pub fn clone_controller(&self) -> Arc<Mutex<ControllerState>> {
Arc::clone(&self.controller_state)
}
pub fn clone_led(&self) -> Arc<Mutex<LedState>> {
Arc::clone(&self.led_state)
}
}
impl Clone for FullState {
fn clone(&self) -> Self {
Self {
controller_state: Arc::clone(&self.controller_state),
led_state: Arc::clone(&self.led_state),
}
}
}
-128
View File
@@ -1,128 +0,0 @@
use std::{
ops::DerefMut,
thread,
time::{Duration, Instant},
};
use palette::{FromColor, Hsv, Srgb};
use crate::slider_io::{
config::{LedMode, ReactiveLayout},
controller_state::{FullState, LedState},
worker::Job,
};
pub struct LedJob {
state: FullState,
mode: LedMode,
sensitivity: u8,
splits: usize,
buttons_per_split: usize,
}
impl LedJob {
pub fn new(state: &FullState, mode: &LedMode) -> Self {
let mut splits = 16;
let mut sensitivity = 20;
match mode {
LedMode::Reactive {
layout,
sensitivity: sensitivity_ref,
} => {
sensitivity = *sensitivity_ref;
splits = match layout {
ReactiveLayout::Four => 4,
ReactiveLayout::Eight => 8,
ReactiveLayout::Sixteen => 16,
}
}
_ => {}
};
Self {
state: state.clone(),
mode: mode.clone(),
sensitivity,
splits,
buttons_per_split: 32 / splits,
}
}
fn calc_lights(&self, flat_controller_state: &Vec<bool>, led_state: &mut LedState) {
match self.mode {
LedMode::Reactive { .. } => {
let banks: Vec<bool> = flat_controller_state
.chunks(self.buttons_per_split)
.take(self.splits)
.map(|x| x.iter().any(|x| *x))
.collect();
led_state
.led_state
.chunks_mut(3)
.enumerate()
.for_each(|(idx, chunk)| match (idx + 1) % self.buttons_per_split {
0 => {
chunk[0] = 255;
chunk[1] = 0;
chunk[2] = 255;
}
_ => match banks[idx / self.buttons_per_split] {
true => {
chunk[0] = 255;
chunk[1] = 0;
chunk[2] = 255;
}
false => {
chunk[0] = 255;
chunk[1] = 255;
chunk[2] = 0;
}
},
})
}
LedMode::Attract => {
let now = Instant::now();
let theta = (now - led_state.start).div_duration_f64(Duration::from_secs(4)) % 1.0;
led_state
.led_state
.chunks_mut(3)
.enumerate()
.for_each(|(idx, chunk)| {
let slice_theta = (&theta + (idx as f64) / 31.0) % 1.0;
let color =
Srgb::from_color(Hsv::new(slice_theta * 360.0, 1.0, 1.0)).into_format::<u8>();
chunk[0] = color.red;
chunk[1] = color.green;
chunk[2] = color.blue;
});
}
_ => panic!("Not implemented"),
}
led_state.dirty = true;
}
}
impl Job for LedJob {
fn setup(&mut self) {}
fn tick(&mut self) {
let flat_controller_state: Vec<bool>;
{
let controller_state_handle = self.state.controller_state.lock().unwrap();
flat_controller_state = controller_state_handle.flat(&self.sensitivity);
}
{
let mut led_state_handle = self.state.led_state.lock().unwrap();
self.calc_lights(&flat_controller_state, led_state_handle.deref_mut());
}
thread::sleep(Duration::from_millis(33));
}
fn teardown(&mut self) {}
}
-33
View File
@@ -1,33 +0,0 @@
use log::info;
use crate::slider_io::{
config::Config, controller_state::FullState, device::HidDeviceJob, led::LedJob,
output::KeyboardOutputJob, worker::Worker,
};
pub struct Manager {
state: FullState,
config: Config,
device_worker: Worker,
output_worker: Worker,
led_worker: Worker,
}
impl Manager {
pub fn new(config: Config) -> Self {
let state = FullState::new();
let device_worker = Worker::new(HidDeviceJob::from_config(&state, &config.device_mode));
let output_worker = Worker::new(KeyboardOutputJob::new(&state, &config.output_mode));
let led_worker = Worker::new(LedJob::new(&state, &config.led_mode));
info!("Starting manager with config: {:?}", config);
Self {
state,
config,
device_worker,
output_worker,
led_worker,
}
}
}
-15
View File
@@ -1,15 +0,0 @@
mod config;
mod controller_state;
mod worker;
mod keyboard;
mod device;
mod led;
mod output;
mod manager;
pub use config::Config;
pub use manager::Manager;
-46
View File
@@ -1,46 +0,0 @@
use std::{thread, time::Duration};
use crate::slider_io::{
config::OutputMode, controller_state::FullState, keyboard::KeyboardOutput, worker::Job,
};
pub struct KeyboardOutputJob {
state: FullState,
sensitivity: u8,
keyboard_output: KeyboardOutput,
}
impl KeyboardOutputJob {
pub fn new(state: &FullState, mode: &OutputMode) -> Self {
match mode {
OutputMode::Keyboard {
layout,
sensitivity,
} => Self {
state: state.clone(),
sensitivity: *sensitivity,
keyboard_output: KeyboardOutput::new(layout.clone()),
},
_ => panic!("Not implemented"),
}
}
}
impl Job for KeyboardOutputJob {
fn setup(&mut self) {}
fn tick(&mut self) {
let flat_controller_state: Vec<bool>;
{
let controller_state_handle = self.state.controller_state.lock().unwrap();
flat_controller_state = controller_state_handle.flat(&self.sensitivity);
}
self.keyboard_output.tick(&flat_controller_state);
thread::sleep(Duration::from_millis(10));
}
fn teardown(&mut self) {
self.keyboard_output.reset();
}
}
-48
View File
@@ -1,48 +0,0 @@
use std::{
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
};
pub trait Job: Send {
fn setup(&mut self);
fn tick(&mut self);
fn teardown(&mut self);
}
pub struct Worker {
thread: Option<thread::JoinHandle<()>>,
stop_signal: Arc<AtomicBool>,
}
impl Worker {
pub fn new<T: 'static + Job>(mut job: T) -> Self {
let stop_signal = Arc::new(AtomicBool::new(false));
let stop_signal_clone = Arc::clone(&stop_signal);
Self {
thread: Some(thread::spawn(move || {
job.setup();
loop {
job.tick();
if stop_signal_clone.load(Ordering::SeqCst) {
break;
}
}
job.teardown();
})),
stop_signal,
}
}
}
impl Drop for Worker {
fn drop(&mut self) {
self.stop_signal.swap(true, Ordering::SeqCst);
if self.thread.is_some() {
self.thread.take().unwrap().join().ok();
}
}
}
+13 -9
View File
@@ -1,7 +1,7 @@
{
"package": {
"productName": "slidershim",
"version": "0.1.0"
"version": "0.2.0"
},
"build": {
"distDir": "../public",
@@ -13,7 +13,7 @@
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.tauri.dev",
"identifier": "me.ress.imp.slidershim",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
@@ -21,12 +21,14 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [],
"resources": [
"./README.txt"
],
"externalBin": [],
"copyright": "",
"copyright": "© 4yn 2022",
"category": "DeveloperTool",
"shortDescription": "",
"longDescription": "",
"shortDescription": "slidershim",
"longDescription": "slidershim",
"deb": {
"depends": [],
"useBootstrapper": false
@@ -49,15 +51,17 @@
"active": false
},
"allowlist": {
"all": true
"all": false
},
"windows": [
{
"title": "slidershim",
"width": 500,
"height": 400,
"height": 550,
"resizable": false,
"fullscreen": false
"fullscreen": false,
"decorations": false,
"transparent": true
}
],
"security": {
+1
View File
@@ -0,0 +1 @@
target/
+56
View File
@@ -0,0 +1,56 @@
{
"files.associations": {
"algorithm": "cpp",
"array": "cpp",
"atomic": "cpp",
"bit": "cpp",
"cctype": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"compare": "cpp",
"concepts": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"exception": "cpp",
"functional": "cpp",
"initializer_list": "cpp",
"ios": "cpp",
"iosfwd": "cpp",
"istream": "cpp",
"iterator": "cpp",
"limits": "cpp",
"list": "cpp",
"memory": "cpp",
"new": "cpp",
"ostream": "cpp",
"set": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"string": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"typeinfo": "cpp",
"unordered_map": "cpp",
"utility": "cpp",
"vector": "cpp",
"xfacet": "cpp",
"xhash": "cpp",
"xiosbase": "cpp",
"xlocale": "cpp",
"xlocinfo": "cpp",
"xlocnum": "cpp",
"xmemory": "cpp",
"xstddef": "cpp",
"xstring": "cpp",
"xtr1common": "cpp",
"xtree": "cpp",
"xutility": "cpp"
}
}
+173
View File
@@ -0,0 +1,173 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "cxx"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0432498c7382a83e9d40a7e4293cdd789ca561aac0d0c17ddb32e3627d989b"
dependencies = [
"cc",
"cxxbridge-flags",
"cxxbridge-macro",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "966ac3dae5d57d58b4240cd2038b9ef2afbfc9e77474f9308242bf0ba346239a"
dependencies = [
"cc",
"codespan-reporting",
"once_cell",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63125c8c1bd5203a8dbf572e502c220383d409e8c287ae4bc455c2bc37de9223"
[[package]]
name = "cxxbridge-macro"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd893a7a7317226890316f59576112030de3484dca8573fe0d6c28323902697"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "link-cplusplus"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cae2cd7ba2f3f63938b9c724475dfb7b9861b545a90324476324ed21dbc8c8"
dependencies = [
"cc",
]
[[package]]
name = "once_cell"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "proc-macro2"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
[[package]]
name = "scratch"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96311ef4a16462c757bb6a39152c40f58f31cd2602a40fceb937e2bc34e6cbab"
[[package]]
name = "syn"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "wwserial"
version = "0.1.0"
dependencies = [
"cxx",
"cxx-build",
]
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "wwserial"
version = "0.1.0"
edition = "2018"
publish = false
[dependencies]
cxx = "1.0"
[build-dependencies]
cxx-build = "1.0"
+22
View File
@@ -0,0 +1,22 @@
fn main() {
cxx_build::bridge("src/lib.rs")
.file("src/serial.cc")
.file("src/serial_win.cc")
.file("src/list_ports_win.cc")
.file("src/wwserial.cc")
.flag_if_supported("-std=c++14")
.compile("cxxbridge-demo");
println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-changed=include/serial.h");
println!("cargo:rerun-if-changed=include/serial_win.h");
println!("cargo:rerun-if-changed=include/v8stdint.cc");
println!("cargo:rerun-if-changed=src/serial.cc");
println!("cargo:rerun-if-changed=src/serial_win.cc");
println!("cargo:rerun-if-changed=src/list_ports_win.cc");
println!("cargo:rerun-if-changed=src/wwserial.cc");
println!("cargo:rerun-if-changed=include/wwserial.h");
}
+777
View File
@@ -0,0 +1,777 @@
/*!
* \file serial/serial.h
* \author William Woodall <wjwwood@gmail.com>
* \author John Harrison <ash.gti@gmail.com>
* \version 0.1
*
* \section LICENSE
*
* The MIT License
*
* Copyright (c) 2012 William Woodall
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* \section DESCRIPTION
*
* This provides a cross platform interface for interacting with Serial Ports.
*/
#ifndef SERIAL_H
#define SERIAL_H
#include <limits>
#include <vector>
#include <string>
#include <cstring>
#include <sstream>
#include <exception>
#include <stdexcept>
// #include <serial/v8stdint.h>
#include "wwserial/include/v8stdint.h"
#define THROW(exceptionClass, message) throw exceptionClass(__FILE__, \
__LINE__, (message) )
namespace serial {
/*!
* Enumeration defines the possible bytesizes for the serial port.
*/
typedef enum {
fivebits = 5,
sixbits = 6,
sevenbits = 7,
eightbits = 8
} bytesize_t;
/*!
* Enumeration defines the possible parity types for the serial port.
*/
typedef enum {
parity_none = 0,
parity_odd = 1,
parity_even = 2,
parity_mark = 3,
parity_space = 4
} parity_t;
/*!
* Enumeration defines the possible stopbit types for the serial port.
*/
typedef enum {
stopbits_one = 1,
stopbits_two = 2,
stopbits_one_point_five
} stopbits_t;
/*!
* Enumeration defines the possible flowcontrol types for the serial port.
*/
typedef enum {
flowcontrol_none = 0,
flowcontrol_software,
flowcontrol_hardware
} flowcontrol_t;
/*!
* Structure for setting the timeout of the serial port, times are
* in milliseconds.
*
* In order to disable the interbyte timeout, set it to Timeout::max().
*/
struct Timeout {
#ifdef max
# undef max
#endif
static uint32_t max() {return std::numeric_limits<uint32_t>::max();}
/*!
* Convenience function to generate Timeout structs using a
* single absolute timeout.
*
* \param timeout A long that defines the time in milliseconds until a
* timeout occurs after a call to read or write is made.
*
* \return Timeout struct that represents this simple timeout provided.
*/
static Timeout simpleTimeout(uint32_t timeout) {
return Timeout(max(), timeout, 0, timeout, 0);
}
/*! Number of milliseconds between bytes received to timeout on. */
uint32_t inter_byte_timeout;
/*! A constant number of milliseconds to wait after calling read. */
uint32_t read_timeout_constant;
/*! A multiplier against the number of requested bytes to wait after
* calling read.
*/
uint32_t read_timeout_multiplier;
/*! A constant number of milliseconds to wait after calling write. */
uint32_t write_timeout_constant;
/*! A multiplier against the number of requested bytes to wait after
* calling write.
*/
uint32_t write_timeout_multiplier;
explicit Timeout (uint32_t inter_byte_timeout_=0,
uint32_t read_timeout_constant_=0,
uint32_t read_timeout_multiplier_=0,
uint32_t write_timeout_constant_=0,
uint32_t write_timeout_multiplier_=0)
: inter_byte_timeout(inter_byte_timeout_),
read_timeout_constant(read_timeout_constant_),
read_timeout_multiplier(read_timeout_multiplier_),
write_timeout_constant(write_timeout_constant_),
write_timeout_multiplier(write_timeout_multiplier_)
{}
};
/*!
* Class that provides a portable serial port interface.
*/
class Serial {
public:
/*!
* Creates a Serial object and opens the port if a port is specified,
* otherwise it remains closed until serial::Serial::open is called.
*
* \param port A std::string containing the address of the serial port,
* which would be something like 'COM1' on Windows and '/dev/ttyS0'
* on Linux.
*
* \param baudrate An unsigned 32-bit integer that represents the baudrate
*
* \param timeout A serial::Timeout struct that defines the timeout
* conditions for the serial port. \see serial::Timeout
*
* \param bytesize Size of each byte in the serial transmission of data,
* default is eightbits, possible values are: fivebits, sixbits, sevenbits,
* eightbits
*
* \param parity Method of parity, default is parity_none, possible values
* are: parity_none, parity_odd, parity_even
*
* \param stopbits Number of stop bits used, default is stopbits_one,
* possible values are: stopbits_one, stopbits_one_point_five, stopbits_two
*
* \param flowcontrol Type of flowcontrol used, default is
* flowcontrol_none, possible values are: flowcontrol_none,
* flowcontrol_software, flowcontrol_hardware
*
* \throw serial::PortNotOpenedException
* \throw serial::IOException
* \throw std::invalid_argument
*/
Serial (const std::string &port = "",
uint32_t baudrate = 9600,
Timeout timeout = Timeout(),
bytesize_t bytesize = eightbits,
parity_t parity = parity_none,
stopbits_t stopbits = stopbits_one,
flowcontrol_t flowcontrol = flowcontrol_none);
/*! Destructor */
virtual ~Serial ();
/*!
* Opens the serial port as long as the port is set and the port isn't
* already open.
*
* If the port is provided to the constructor then an explicit call to open
* is not needed.
*
* \see Serial::Serial
*
* \throw std::invalid_argument
* \throw serial::SerialException
* \throw serial::IOException
*/
void
open ();
/*! Gets the open status of the serial port.
*
* \return Returns true if the port is open, false otherwise.
*/
bool
isOpen () const;
/*! Closes the serial port. */
void
close ();
/*! Return the number of characters in the buffer. */
size_t
available ();
/*! Block until there is serial data to read or read_timeout_constant
* number of milliseconds have elapsed. The return value is true when
* the function exits with the port in a readable state, false otherwise
* (due to timeout or select interruption). */
bool
waitReadable ();
/*! Block for a period of time corresponding to the transmission time of
* count characters at present serial settings. This may be used in con-
* junction with waitReadable to read larger blocks of data from the
* port. */
void
waitByteTimes (size_t count);
/*! Read a given amount of bytes from the serial port into a given buffer.
*
* The read function will return in one of three cases:
* * The number of requested bytes was read.
* * In this case the number of bytes requested will match the size_t
* returned by read.
* * A timeout occurred, in this case the number of bytes read will not
* match the amount requested, but no exception will be thrown. One of
* two possible timeouts occurred:
* * The inter byte timeout expired, this means that number of
* milliseconds elapsed between receiving bytes from the serial port
* exceeded the inter byte timeout.
* * The total timeout expired, which is calculated by multiplying the
* read timeout multiplier by the number of requested bytes and then
* added to the read timeout constant. If that total number of
* milliseconds elapses after the initial call to read a timeout will
* occur.
* * An exception occurred, in this case an actual exception will be thrown.
*
* \param buffer An uint8_t array of at least the requested size.
* \param size A size_t defining how many bytes to be read.
*
* \return A size_t representing the number of bytes read as a result of the
* call to read.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
*/
size_t
read (uint8_t *buffer, size_t size);
/*! Read a given amount of bytes from the serial port into a give buffer.
*
* \param buffer A reference to a std::vector of uint8_t.
* \param size A size_t defining how many bytes to be read.
*
* \return A size_t representing the number of bytes read as a result of the
* call to read.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
*/
size_t
read (std::vector<uint8_t> &buffer, size_t size = 1);
/*! Read a given amount of bytes from the serial port into a give buffer.
*
* \param buffer A reference to a std::string.
* \param size A size_t defining how many bytes to be read.
*
* \return A size_t representing the number of bytes read as a result of the
* call to read.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
*/
size_t
read (std::string &buffer, size_t size = 1);
/*! Read a given amount of bytes from the serial port and return a string
* containing the data.
*
* \param size A size_t defining how many bytes to be read.
*
* \return A std::string containing the data read from the port.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
*/
std::string
read (size_t size = 1);
/*! Reads in a line or until a given delimiter has been processed.
*
* Reads from the serial port until a single line has been read.
*
* \param buffer A std::string reference used to store the data.
* \param size A maximum length of a line, defaults to 65536 (2^16)
* \param eol A string to match against for the EOL.
*
* \return A size_t representing the number of bytes read.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
*/
size_t
readline (std::string &buffer, size_t size = 65536, std::string eol = "\n");
/*! Reads in a line or until a given delimiter has been processed.
*
* Reads from the serial port until a single line has been read.
*
* \param size A maximum length of a line, defaults to 65536 (2^16)
* \param eol A string to match against for the EOL.
*
* \return A std::string containing the line.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
*/
std::string
readline (size_t size = 65536, std::string eol = "\n");
/*! Reads in multiple lines until the serial port times out.
*
* This requires a timeout > 0 before it can be run. It will read until a
* timeout occurs and return a list of strings.
*
* \param size A maximum length of combined lines, defaults to 65536 (2^16)
*
* \param eol A string to match against for the EOL.
*
* \return A vector<string> containing the lines.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
*/
std::vector<std::string>
readlines (size_t size = 65536, std::string eol = "\n");
/*! Write a string to the serial port.
*
* \param data A const reference containing the data to be written
* to the serial port.
*
* \param size A size_t that indicates how many bytes should be written from
* the given data buffer.
*
* \return A size_t representing the number of bytes actually written to
* the serial port.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
* \throw serial::IOException
*/
size_t
write (const uint8_t *data, size_t size);
/*! Write a string to the serial port.
*
* \param data A const reference containing the data to be written
* to the serial port.
*
* \return A size_t representing the number of bytes actually written to
* the serial port.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
* \throw serial::IOException
*/
size_t
write (const std::vector<uint8_t> &data);
/*! Write a string to the serial port.
*
* \param data A const reference containing the data to be written
* to the serial port.
*
* \return A size_t representing the number of bytes actually written to
* the serial port.
*
* \throw serial::PortNotOpenedException
* \throw serial::SerialException
* \throw serial::IOException
*/
size_t
write (const std::string &data);
/*! Sets the serial port identifier.
*
* \param port A const std::string reference containing the address of the
* serial port, which would be something like 'COM1' on Windows and
* '/dev/ttyS0' on Linux.
*
* \throw std::invalid_argument
*/
void
setPort (const std::string &port);
/*! Gets the serial port identifier.
*
* \see Serial::setPort
*
* \throw std::invalid_argument
*/
std::string
getPort () const;
/*! Sets the timeout for reads and writes using the Timeout struct.
*
* There are two timeout conditions described here:
* * The inter byte timeout:
* * The inter_byte_timeout component of serial::Timeout defines the
* maximum amount of time, in milliseconds, between receiving bytes on
* the serial port that can pass before a timeout occurs. Setting this
* to zero will prevent inter byte timeouts from occurring.
* * Total time timeout:
* * The constant and multiplier component of this timeout condition,
* for both read and write, are defined in serial::Timeout. This
* timeout occurs if the total time since the read or write call was
* made exceeds the specified time in milliseconds.
* * The limit is defined by multiplying the multiplier component by the
* number of requested bytes and adding that product to the constant
* component. In this way if you want a read call, for example, to
* timeout after exactly one second regardless of the number of bytes
* you asked for then set the read_timeout_constant component of
* serial::Timeout to 1000 and the read_timeout_multiplier to zero.
* This timeout condition can be used in conjunction with the inter
* byte timeout condition with out any problems, timeout will simply
* occur when one of the two timeout conditions is met. This allows
* users to have maximum control over the trade-off between
* responsiveness and efficiency.
*
* Read and write functions will return in one of three cases. When the
* reading or writing is complete, when a timeout occurs, or when an
* exception occurs.
*
* A timeout of 0 enables non-blocking mode.
*
* \param timeout A serial::Timeout struct containing the inter byte
* timeout, and the read and write timeout constants and multipliers.
*
* \see serial::Timeout
*/
void
setTimeout (Timeout &timeout);
/*! Sets the timeout for reads and writes. */
void
setTimeout (uint32_t inter_byte_timeout, uint32_t read_timeout_constant,
uint32_t read_timeout_multiplier, uint32_t write_timeout_constant,
uint32_t write_timeout_multiplier)
{
Timeout timeout(inter_byte_timeout, read_timeout_constant,
read_timeout_multiplier, write_timeout_constant,
write_timeout_multiplier);
return setTimeout(timeout);
}
/*! Gets the timeout for reads in seconds.
*
* \return A Timeout struct containing the inter_byte_timeout, and read
* and write timeout constants and multipliers.
*
* \see Serial::setTimeout
*/
Timeout
getTimeout () const;
/*! Sets the baudrate for the serial port.
*
* Possible baudrates depends on the system but some safe baudrates include:
* 110, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 28800, 38400, 56000,
* 57600, 115200
* Some other baudrates that are supported by some comports:
* 128000, 153600, 230400, 256000, 460800, 500000, 921600
*
* \param baudrate An integer that sets the baud rate for the serial port.
*
* \throw std::invalid_argument
*/
void
setBaudrate (uint32_t baudrate);
/*! Gets the baudrate for the serial port.
*
* \return An integer that sets the baud rate for the serial port.
*
* \see Serial::setBaudrate
*
* \throw std::invalid_argument
*/
uint32_t
getBaudrate () const;
/*! Sets the bytesize for the serial port.
*
* \param bytesize Size of each byte in the serial transmission of data,
* default is eightbits, possible values are: fivebits, sixbits, sevenbits,
* eightbits
*
* \throw std::invalid_argument
*/
void
setBytesize (bytesize_t bytesize);
/*! Gets the bytesize for the serial port.
*
* \see Serial::setBytesize
*
* \throw std::invalid_argument
*/
bytesize_t
getBytesize () const;
/*! Sets the parity for the serial port.
*
* \param parity Method of parity, default is parity_none, possible values
* are: parity_none, parity_odd, parity_even
*
* \throw std::invalid_argument
*/
void
setParity (parity_t parity);
/*! Gets the parity for the serial port.
*
* \see Serial::setParity
*
* \throw std::invalid_argument
*/
parity_t
getParity () const;
/*! Sets the stopbits for the serial port.
*
* \param stopbits Number of stop bits used, default is stopbits_one,
* possible values are: stopbits_one, stopbits_one_point_five, stopbits_two
*
* \throw std::invalid_argument
*/
void
setStopbits (stopbits_t stopbits);
/*! Gets the stopbits for the serial port.
*
* \see Serial::setStopbits
*
* \throw std::invalid_argument
*/
stopbits_t
getStopbits () const;
/*! Sets the flow control for the serial port.
*
* \param flowcontrol Type of flowcontrol used, default is flowcontrol_none,
* possible values are: flowcontrol_none, flowcontrol_software,
* flowcontrol_hardware
*
* \throw std::invalid_argument
*/
void
setFlowcontrol (flowcontrol_t flowcontrol);
/*! Gets the flow control for the serial port.
*
* \see Serial::setFlowcontrol
*
* \throw std::invalid_argument
*/
flowcontrol_t
getFlowcontrol () const;
/*! Flush the input and output buffers */
void
flush ();
/*! Flush only the input buffer */
void
flushInput ();
/*! Flush only the output buffer */
void
flushOutput ();
/*! Sends the RS-232 break signal. See tcsendbreak(3). */
void
sendBreak (int duration);
/*! Set the break condition to a given level. Defaults to true. */
void
setBreak (bool level = true);
/*! Set the RTS handshaking line to the given level. Defaults to true. */
void
setRTS (bool level = true);
/*! Set the DTR handshaking line to the given level. Defaults to true. */
void
setDTR (bool level = true);
/*!
* Blocks until CTS, DSR, RI, CD changes or something interrupts it.
*
* Can throw an exception if an error occurs while waiting.
* You can check the status of CTS, DSR, RI, and CD once this returns.
* Uses TIOCMIWAIT via ioctl if available (mostly only on Linux) with a
* resolution of less than +-1ms and as good as +-0.2ms. Otherwise a
* polling method is used which can give +-2ms.
*
* \return Returns true if one of the lines changed, false if something else
* occurred.
*
* \throw SerialException
*/
bool
waitForChange ();
/*! Returns the current status of the CTS line. */
bool
getCTS ();
/*! Returns the current status of the DSR line. */
bool
getDSR ();
/*! Returns the current status of the RI line. */
bool
getRI ();
/*! Returns the current status of the CD line. */
bool
getCD ();
private:
// Disable copy constructors
Serial(const Serial&);
Serial& operator=(const Serial&);
// Pimpl idiom, d_pointer
class SerialImpl;
SerialImpl *pimpl_;
// Scoped Lock Classes
class ScopedReadLock;
class ScopedWriteLock;
// Read common function
size_t
read_ (uint8_t *buffer, size_t size);
// Write common function
size_t
write_ (const uint8_t *data, size_t length);
};
class SerialException : public std::exception
{
// Disable copy constructors
SerialException& operator=(const SerialException&);
std::string e_what_;
public:
SerialException (const char *description) {
std::stringstream ss;
ss << "SerialException " << description << " failed.";
e_what_ = ss.str();
}
SerialException (const SerialException& other) : e_what_(other.e_what_) {}
virtual ~SerialException() throw() {}
virtual const char* what () const throw () {
return e_what_.c_str();
}
};
class IOException : public std::exception
{
// Disable copy constructors
IOException& operator=(const IOException&);
std::string file_;
int line_;
std::string e_what_;
int errno_;
public:
explicit IOException (std::string file, int line, int errnum)
: file_(file), line_(line), errno_(errnum) {
std::stringstream ss;
#if defined(_WIN32) && !defined(__MINGW32__)
char error_str [1024];
strerror_s(error_str, 1024, errnum);
#else
char * error_str = strerror(errnum);
#endif
ss << "IO Exception (" << errno_ << "): " << error_str;
ss << ", file " << file_ << ", line " << line_ << ".";
e_what_ = ss.str();
}
explicit IOException (std::string file, int line, const char * description)
: file_(file), line_(line), errno_(0) {
std::stringstream ss;
ss << "IO Exception: " << description;
ss << ", file " << file_ << ", line " << line_ << ".";
e_what_ = ss.str();
}
virtual ~IOException() throw() {}
IOException (const IOException& other) : line_(other.line_), e_what_(other.e_what_), errno_(other.errno_) {}
int getErrorNumber () const { return errno_; }
virtual const char* what () const throw () {
return e_what_.c_str();
}
};
class PortNotOpenedException : public std::exception
{
// Disable copy constructors
const PortNotOpenedException& operator=(PortNotOpenedException);
std::string e_what_;
public:
PortNotOpenedException (const char * description) {
std::stringstream ss;
ss << "PortNotOpenedException " << description << " failed.";
e_what_ = ss.str();
}
PortNotOpenedException (const PortNotOpenedException& other) : e_what_(other.e_what_) {}
virtual ~PortNotOpenedException() throw() {}
virtual const char* what () const throw () {
return e_what_.c_str();
}
};
/*!
* Structure that describes a serial device.
*/
struct PortInfo {
/*! Address of the serial port (this can be passed to the constructor of Serial). */
std::string port;
/*! Human readable description of serial device if available. */
std::string description;
/*! Hardware ID (e.g. VID:PID of USB serial devices) or "n/a" if not available. */
std::string hardware_id;
};
/* Lists the serial ports available on the system
*
* Returns a vector of available serial ports, each represented
* by a serial::PortInfo data structure:
*
* \return vector of serial::PortInfo.
*/
std::vector<PortInfo>
list_ports();
} // namespace serial
#endif
+208
View File
@@ -0,0 +1,208 @@
/*!
* \file serial/impl/windows.h
* \author William Woodall <wjwwood@gmail.com>
* \author John Harrison <ash@greaterthaninfinity.com>
* \version 0.1
*
* \section LICENSE
*
* The MIT License
*
* Copyright (c) 2012 William Woodall, John Harrison
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* \section DESCRIPTION
*
* This provides a windows implementation of the Serial class interface.
*
*/
#if defined(_WIN32)
#ifndef SERIAL_IMPL_WINDOWS_H
#define SERIAL_IMPL_WINDOWS_H
// #include "serial/serial.h"
#include "wwserial/include/serial.h"
#include "windows.h"
namespace serial {
using std::string;
using std::wstring;
using std::invalid_argument;
using serial::SerialException;
using serial::IOException;
class serial::Serial::SerialImpl {
public:
SerialImpl (const string &port,
unsigned long baudrate,
bytesize_t bytesize,
parity_t parity,
stopbits_t stopbits,
flowcontrol_t flowcontrol);
virtual ~SerialImpl ();
void
open ();
void
close ();
bool
isOpen () const;
size_t
available ();
bool
waitReadable (uint32_t timeout);
void
waitByteTimes (size_t count);
size_t
read (uint8_t *buf, size_t size = 1);
size_t
write (const uint8_t *data, size_t length);
void
flush ();
void
flushInput ();
void
flushOutput ();
void
sendBreak (int duration);
void
setBreak (bool level);
void
setRTS (bool level);
void
setDTR (bool level);
bool
waitForChange ();
bool
getCTS ();
bool
getDSR ();
bool
getRI ();
bool
getCD ();
void
setPort (const string &port);
string
getPort () const;
void
setTimeout (Timeout &timeout);
Timeout
getTimeout () const;
void
setBaudrate (unsigned long baudrate);
unsigned long
getBaudrate () const;
void
setBytesize (bytesize_t bytesize);
bytesize_t
getBytesize () const;
void
setParity (parity_t parity);
parity_t
getParity () const;
void
setStopbits (stopbits_t stopbits);
stopbits_t
getStopbits () const;
void
setFlowcontrol (flowcontrol_t flowcontrol);
flowcontrol_t
getFlowcontrol () const;
void
readLock ();
void
readUnlock ();
void
writeLock ();
void
writeUnlock ();
protected:
void reconfigurePort ();
private:
wstring port_; // Path to the file descriptor
HANDLE fd_;
bool is_open_;
Timeout timeout_; // Timeout for read operations
unsigned long baudrate_; // Baudrate
parity_t parity_; // Parity
bytesize_t bytesize_; // Size of the bytes
stopbits_t stopbits_; // Stop Bits
flowcontrol_t flowcontrol_; // Flow Control
// Mutex used to lock the read functions
HANDLE read_mutex;
// Mutex used to lock the write functions
HANDLE write_mutex;
};
}
#endif // SERIAL_IMPL_WINDOWS_H
#endif // if defined(_WIN32)
+57
View File
@@ -0,0 +1,57 @@
// This header is from the v8 google project:
// http://code.google.com/p/v8/source/browse/trunk/include/v8stdint.h
// Copyright 2012 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Load definitions of standard types.
#ifndef V8STDINT_H_
#define V8STDINT_H_
#include <stddef.h>
#include <stdio.h>
#if defined(_WIN32) && !defined(__MINGW32__)
typedef signed char int8_t;
typedef unsigned char uint8_t;
typedef short int16_t; // NOLINT
typedef unsigned short uint16_t; // NOLINT
typedef int int32_t;
typedef unsigned int uint32_t;
typedef __int64 int64_t;
typedef unsigned __int64 uint64_t;
// intptr_t and friends are defined in crtdefs.h through stdio.h.
#else
#include <stdint.h>
#endif
#endif // V8STDINT_H_
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "rust/cxx.h"
#include <memory>
#include <string>
struct CxxSerial
{
CxxSerial(rust::String port, uint32_t baud, uint32_t read_timeout, uint32_t write_timeout, bool hardware);
uint32_t write(const rust::Vec<uint8_t> &data) const;
uint32_t read(rust::Vec<uint8_t> &data) const;
void flush() const;
bool check() const;
private:
struct impl;
std::shared_ptr<impl> impl;
};
// port: String, baud: u32, timeout: u32, hardware: bool
std::unique_ptr<CxxSerial> new_cxx_serial(rust::String port, uint32_t baud, uint32_t read_timeout, uint32_t write_timeout, bool hardware);

Some files were not shown because too many files have changed in this diff Show More