Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d02990d5fc | ||
|
|
dd98fe5579 | ||
|
|
7178e614b1 | ||
|
|
0029dad2f2 | ||
|
|
08b1b92f52 | ||
|
|
24d920c5bf | ||
|
|
f396a21999 | ||
|
|
2782d406dd | ||
|
|
5865cd9d21 | ||
|
|
6bc462210f | ||
|
|
f356624502 | ||
|
|
287c9c63f6 | ||
|
|
ac4933449b | ||
|
|
2a0d466cf3 | ||
|
|
0772285761 | ||
|
|
2539b9ad23 | ||
|
|
2d56431fe2 | ||
|
|
4dbeb432ec | ||
|
|
4f4d951610 | ||
|
|
3d20472c7f | ||
|
|
c502941251 | ||
|
|
885ac1e77c | ||
|
|
e2dec75e6b | ||
|
|
2ae7a81f22 | ||
|
|
afa5a46c65 | ||
|
|
6f052554ce | ||
|
|
da00ed9afa | ||
|
|
51a77c76fa | ||
|
|
3ba7f2b8f2 | ||
|
|
331d23a674 | ||
|
|
3acf511da8 | ||
|
|
98509250d6 | ||
|
|
a5c89f00cb | ||
|
|
cc24e628aa | ||
|
|
fcf80538b4 | ||
|
|
a8d72207a4 | ||
|
|
21afaa661b | ||
|
|
91030cb96b | ||
|
|
8a67094c9a | ||
|
|
bfb7f7dc95 | ||
|
|
263d21c0a6 | ||
|
|
786ac83c4c | ||
|
|
7f155903ca | ||
|
|
281e4b02b1 | ||
|
|
93240ef4af | ||
|
|
2b20902179 |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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&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
|
||||
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,5 @@
|
||||
```
|
||||
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
$env:RUST_BACKTRACE = "1";
|
||||
yarn tauri dev
|
||||
```
|
||||
@@ -28,6 +28,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.0.0-beta.8",
|
||||
"sharp": "^0.30.0",
|
||||
"sirv-cli": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -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>
|
||||
|
||||
@@ -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 /><<</div>
|
||||
<div style="grid-area: lr">LS<br />>></div>
|
||||
<div style="grid-area: rl">RS<br /><<</div>
|
||||
<div style="grid-area: rr">RS<br />>></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></div>
|
||||
<div style="grid-area: rl">RS<</div>
|
||||
<div style="grid-area: lr">LS></div>
|
||||
<div style="grid-area: ll">LS<</div>
|
||||
</div>
|
||||
<div class="extra">
|
||||
<div>[start]</div>
|
||||
<div>[back]</div>
|
||||
<div>[guide]</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -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>
|
||||
@@ -0,0 +1 @@
|
||||
/target/
|
||||
@@ -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"] }
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
// }
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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(" ")
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
};
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
|
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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod config;
|
||||
|
||||
pub mod brokenithm;
|
||||
pub mod diva;
|
||||
pub mod hid;
|
||||
@@ -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};
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod config;
|
||||
|
||||
pub mod lighting;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod config;
|
||||
|
||||
mod gamepad;
|
||||
mod keyboard;
|
||||
|
||||
pub mod output;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod serial;
|
||||
pub mod utils;
|
||||
pub mod voltex;
|
||||
pub mod worker;
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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" ]
|
||||
|
||||
@@ -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
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 582 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 640 B |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 2.5 KiB |
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
target/
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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"
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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_
|
||||
@@ -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);
|
||||