Compare commits
20 Commits
01e2359315
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
606c1bd46b
|
|||
|
6231059b82
|
|||
|
10520db877
|
|||
|
57869b16f7
|
|||
|
e4100decdb
|
|||
|
8a37c1736b
|
|||
|
739f3ec5b4
|
|||
|
a13f73cb99
|
|||
|
80f54299a6
|
|||
|
570a788237
|
|||
|
ee402a1072
|
|||
|
7ee402c1d0
|
|||
| 48a74104eb | |||
| 832b700e06 | |||
| 956d13a00f | |||
| 195c70d526 | |||
| 57bfd63126 | |||
| 0f946fa8b0 | |||
| 203a363a6c | |||
| b298c11d36 |
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Brosef & Tango
|
||||||
|
|
||||||
|
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.
|
||||||
40
app.py
40
app.py
@ -4,9 +4,12 @@ A simple Flask application for interacting with WHSPAH shockers.
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
|
import random
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from flask_limiter.util import get_remote_address
|
||||||
|
from flask_limiter import Limiter
|
||||||
import waitress.server
|
import waitress.server
|
||||||
import flask
|
import flask
|
||||||
|
|
||||||
@ -19,8 +22,15 @@ ACTION_MAP = {'shock': whspah.MODES.SHOCK,
|
|||||||
|
|
||||||
app = flask.Flask(__name__, static_folder='./www/')
|
app = flask.Flask(__name__, static_folder='./www/')
|
||||||
|
|
||||||
|
limiter = Limiter(
|
||||||
|
get_remote_address,
|
||||||
|
app=app,
|
||||||
|
storage_uri="memory://",
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/', defaults={'path': 'index.html'})
|
@app.route('/', defaults={'path': 'index.html'})
|
||||||
@app.route('/<path:path>')
|
@app.route('/<path:path>')
|
||||||
|
@limiter.exempt
|
||||||
def staticPage(path):
|
def staticPage(path):
|
||||||
"""
|
"""
|
||||||
Returns anything requested in the static folder.
|
Returns anything requested in the static folder.
|
||||||
@ -35,6 +45,7 @@ def staticPage(path):
|
|||||||
return flask.send_from_directory(app.static_folder, path)
|
return flask.send_from_directory(app.static_folder, path)
|
||||||
|
|
||||||
@app.route('/transmit', methods=['POST'], strict_slashes=False)
|
@app.route('/transmit', methods=['POST'], strict_slashes=False)
|
||||||
|
@limiter.limit("1/second")
|
||||||
def transmit():
|
def transmit():
|
||||||
"""
|
"""
|
||||||
Transmits the data contained within the POST request through WHSPAH.
|
Transmits the data contained within the POST request through WHSPAH.
|
||||||
@ -48,6 +59,7 @@ def transmit():
|
|||||||
txID = int(data['transmitterID'])
|
txID = int(data['transmitterID'])
|
||||||
channel = int(data['channel'])
|
channel = int(data['channel'])
|
||||||
action = ACTION_MAP[data['action']]
|
action = ACTION_MAP[data['action']]
|
||||||
|
pin = int(data['shockerPin'])
|
||||||
intensity = int(data.get('intensity', 0))
|
intensity = int(data.get('intensity', 0))
|
||||||
lucal = bool(data.get('lucalEncoded', False))
|
lucal = bool(data.get('lucalEncoded', False))
|
||||||
# If any of those failed, return an error.
|
# If any of those failed, return an error.
|
||||||
@ -56,9 +68,16 @@ def transmit():
|
|||||||
'txID: int,\n'+
|
'txID: int,\n'+
|
||||||
'channel: int,\n'+
|
'channel: int,\n'+
|
||||||
'action: "shock", "vibrate" or "beep",\n'+
|
'action: "shock", "vibrate" or "beep",\n'+
|
||||||
|
'pin: int,\n'+
|
||||||
'intensity (optional): int\n'+
|
'intensity (optional): int\n'+
|
||||||
'lucalEncoded (optional): bool'}, 400
|
'lucalEncoded (optional): bool'}, 400
|
||||||
|
|
||||||
|
if pin != app.config['pin']:
|
||||||
|
return {'success': False, 'message': 'Unauthorised'}, 401
|
||||||
|
|
||||||
|
if intensity > app.config['limit']:
|
||||||
|
return {'success': False, 'message': 'Exceeded Safe Limit'}, 406
|
||||||
|
|
||||||
# Send the data to WHSPAH
|
# Send the data to WHSPAH
|
||||||
tx: whspah.Transmitter = app.config['transmitter']
|
tx: whspah.Transmitter = app.config['transmitter']
|
||||||
tx.transmit(txID, channel, action, intensity, lucal)
|
tx.transmit(txID, channel, action, intensity, lucal)
|
||||||
@ -66,6 +85,14 @@ def transmit():
|
|||||||
# Return a success message
|
# Return a success message
|
||||||
return {'success': True}, 200
|
return {'success': True}, 200
|
||||||
|
|
||||||
|
@app.route('/limit', methods=['GET'], strict_slashes=False)
|
||||||
|
@limiter.limit("1/second")
|
||||||
|
def getLimit():
|
||||||
|
"""
|
||||||
|
Simply returns the safe limit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {'success': True, 'limit': app.config['limit']}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Parse console arguments
|
# Parse console arguments
|
||||||
@ -73,6 +100,11 @@ if __name__ == '__main__':
|
|||||||
parser.add_argument('-d', '--debug', action='store_true')
|
parser.add_argument('-d', '--debug', action='store_true')
|
||||||
parser.add_argument('--ip', type=str, default='0.0.0.0')
|
parser.add_argument('--ip', type=str, default='0.0.0.0')
|
||||||
parser.add_argument('--port', type=int, default=8000)
|
parser.add_argument('--port', type=int, default=8000)
|
||||||
|
parser.add_argument('--pin', type=int, default=random.randint(1000, 9999),
|
||||||
|
help='The authorisation pin, defaults to a random number '+
|
||||||
|
'between 1000 and 9999')
|
||||||
|
parser.add_argument('--limit', type=int, default=99,
|
||||||
|
help='The same limit for intensity.')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# Sets the logging version based on --debug
|
# Sets the logging version based on --debug
|
||||||
@ -84,6 +116,14 @@ if __name__ == '__main__':
|
|||||||
# Connect to a WHSPAH device
|
# Connect to a WHSPAH device
|
||||||
app.config['transmitter'] = whspah.Transmitter()
|
app.config['transmitter'] = whspah.Transmitter()
|
||||||
|
|
||||||
|
# Configure pin
|
||||||
|
app.config['pin'] = args.pin
|
||||||
|
print(f'Running with pin {args.pin}.')
|
||||||
|
|
||||||
|
# Configure Limit
|
||||||
|
app.config['limit'] = args.limit
|
||||||
|
print(f'Running with safe limit {args.limit}.')
|
||||||
|
|
||||||
if args.debug:
|
if args.debug:
|
||||||
# If in debug mode, run Flask's built in server
|
# If in debug mode, run Flask's built in server
|
||||||
app.run(host=args.ip, port=args.port, debug=True)
|
app.run(host=args.ip, port=args.port, debug=True)
|
||||||
|
|||||||
@ -1,2 +1,4 @@
|
|||||||
|
Flask-Limiter
|
||||||
waitress
|
waitress
|
||||||
|
whspah
|
||||||
flask
|
flask
|
||||||
BIN
www/img/backgroundtile.png
Normal file
BIN
www/img/backgroundtile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="/styles.css">
|
<link rel="stylesheet" href="/styles.css">
|
||||||
<script src="/main.js"></script>
|
<script src="/main.js"></script>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@ -11,7 +12,26 @@
|
|||||||
<h3>A simple locally hosted web interface for WHSPAH.</h3>
|
<h3>A simple locally hosted web interface for WHSPAH.</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="contentBox">
|
<div class="contentBox">
|
||||||
<p>Just pretend there's a bunch of buttons and sliders here. I'm just kind of yapping to test the CSS. Wow look at that slider and those buttons. So cool.</p>
|
<h2>transmitterID (0-65535)</h2>
|
||||||
|
<input type="number" value="0" min="0" max="65535" id="transmitterIDInput">
|
||||||
|
<h2>Channel Input (1-3)</h2>
|
||||||
|
<input type="number" value="1" min="1" max="3" id="channelIDInput">
|
||||||
|
<h2>LucalEncoded?</h2>
|
||||||
|
<input type="checkbox" id="lucalEncodedInput">
|
||||||
|
<h2>PIN</h2>
|
||||||
|
<input type="number" id="shockerPinInput">
|
||||||
|
<h2>Shock</h2>
|
||||||
|
<input type="range" min="0" max="99" value="0" id="shockIntensity" oninput="document.getElementById('sliderValueFirst').value = this.value" class="sliderInput">
|
||||||
|
<br>
|
||||||
|
<output id="sliderValueFirst" class="sliderValue">0</output>
|
||||||
|
<br>
|
||||||
|
<button onclick="shock();" class="sliderButton">Shock!</button>
|
||||||
|
<h2>Vibrate</h2>
|
||||||
|
<input type="range" min="0" max="99" value="0" id="vibrateIntensity" oninput="document.getElementById('sliderValueSecond').value = this.value" class="sliderInput">
|
||||||
|
<br>
|
||||||
|
<output id="sliderValueSecond" class="sliderValue">0</output>
|
||||||
|
<br>
|
||||||
|
<button onclick="vibrate();" class="sliderButton">Vibrate!</button>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
43
www/main.js
43
www/main.js
@ -1,3 +1,21 @@
|
|||||||
|
async function GET(path) {
|
||||||
|
return new Promise(function (onSuccess, onError) {
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', path);
|
||||||
|
xhr.onreadystatechange = function () {
|
||||||
|
if (xhr.readyState === 4) {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
onSuccess(JSON.parse(xhr.responseText));
|
||||||
|
} else {
|
||||||
|
onError(xhr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.onerror = () => onError(xhr);
|
||||||
|
xhr.send(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function POST(path, data={}) {
|
async function POST(path, data={}) {
|
||||||
return new Promise(function (onSuccess, onError) {
|
return new Promise(function (onSuccess, onError) {
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
@ -24,34 +42,43 @@ function assert(condition, message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function transmit(transmitterID, channel, action, intensity=0, lucalEncoded=false) {
|
async function transmit(transmitterID, channel, action, shockerPin, intensity=0, lucalEncoded=false) {
|
||||||
assert(typeof transmitterID === 'number');
|
assert(typeof transmitterID === 'number');
|
||||||
assert(typeof channel === 'number');
|
assert(typeof channel === 'number');
|
||||||
assert(['shock', 'vibrate', 'beep'].includes(action));
|
assert(['shock', 'vibrate', 'beep'].includes(action));
|
||||||
|
assert(typeof shockerPin === 'number');
|
||||||
assert(typeof intensity === 'number');
|
assert(typeof intensity === 'number');
|
||||||
assert(typeof lucalEncoded === 'boolean');
|
assert(typeof lucalEncoded === 'boolean');
|
||||||
POST('/transmit', {transmitterID: transmitterID, channel: channel, action: action, intensity: intensity, lucalEncoded: lucalEncoded});
|
POST('/transmit', {transmitterID: transmitterID, channel: channel, action: action, shockerPin: shockerPin, intensity: intensity, lucalEncoded: lucalEncoded});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function txFromUI(action, intensity=0) {
|
async function txFromUI(action, intensity=0) {
|
||||||
let transmitterID = Number(document.getElementById('CHANGE_ME').value);
|
let transmitterID = Number(document.getElementById('transmitterIDInput').value);
|
||||||
let channel = Number(document.getElementById('CHANGE_ME').value);
|
let channel = Number(document.getElementById('channelIDInput').value);
|
||||||
let lucalEncoded = document.getElementById('CHANGE_ME').checked;
|
let lucalEncoded = document.getElementById('lucalEncodedInput').checked;
|
||||||
transmit(transmitterID, channel, action, intensity, lucalEncoded);
|
let shockerPin = Number(document.getElementById('shockerPinInput').value);
|
||||||
|
transmit(transmitterID, channel, action, shockerPin, intensity, lucalEncoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function shock() {
|
async function shock() {
|
||||||
let intensity = Number(document.getElementById('CHANGE_ME').value);
|
let intensity = Number(document.getElementById('shockIntensity').value);
|
||||||
txFromUI('shock', intensity);
|
txFromUI('shock', intensity);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function vibrate() {
|
async function vibrate() {
|
||||||
let intensity = Number(document.getElementById('CHANGE_ME').value);
|
let intensity = Number(document.getElementById('vibrateIntensity').value);
|
||||||
txFromUI('vibrate', intensity);
|
txFromUI('vibrate', intensity);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function beep() {
|
async function beep() {
|
||||||
txFromUI('beep');
|
txFromUI('beep');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
GET('/limit').then((data) => {
|
||||||
|
document.getElementById('shockIntensity').max = data.limit;
|
||||||
|
document.getElementById('vibrateIntensity').max = data.limit;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -2,20 +2,25 @@ body {
|
|||||||
/* Anything but light mode, please. */
|
/* Anything but light mode, please. */
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
color: #FFFFFF;
|
color: #FFFFFF;
|
||||||
|
background-image: url("/img/backgroundtile.png");
|
||||||
|
background-size: 10%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.whspahTitle{
|
.whspahTitle{
|
||||||
background-color: #363636;
|
background-color: #363636da;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: max-content;
|
width: fit-content;
|
||||||
margin:0 auto;
|
margin:0 auto;
|
||||||
margin-bottom: 1%;
|
margin-bottom: 1%;
|
||||||
padding: 1%;
|
padding: 1%;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contentBox{
|
.contentBox{
|
||||||
background-color: #363636;
|
background-color: #363636da;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
margin:0 auto;
|
margin:0 auto;
|
||||||
@ -23,4 +28,20 @@ body {
|
|||||||
padding: 1%;
|
padding: 1%;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
|
min-width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderInput{
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderValue{
|
||||||
|
background-color: rgb(31, 31, 31);
|
||||||
|
padding-left: 3%;
|
||||||
|
padding-right: 3%;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderButton{
|
||||||
|
margin: 1%;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user