Merge pull request 'data_read' (#1) from data_read into main

Reviewed-on: #1
This commit is contained in:
accidentallycompetent 2023-11-14 22:53:02 +00:00
commit 21c9b05182
11 changed files with 188 additions and 104 deletions

5
.gitignore vendored
View File

@ -158,7 +158,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
bluewind
bluewind/*

View File

@ -6,10 +6,29 @@ Bluetooth control app for Wahoo Headwind.
## Usage ## Usage
To run the flask app: To run the flask app for development and testing:
```bash ```bash
git clone https://teapot.octopusx.de/accidentallycompetent/bluewind.git
cd bluewind
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install --upgrade pip
pip3 install -r requirements.txt
export FLASK_ADDRESS='<headwind bluetooth address>' export FLASK_ADDRESS='<headwind bluetooth address>'
flask --app api run --host=0.0.0.0 export FLASK_URL='<address and port to bind'
python3 main.py
```
To run the flask app in a permanent deployment:
```bash
git clone https://teapot.octopusx.de/accidentallycompetent/bluewind.git
cd bluewind
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install --upgrade pip
pip3 install -r requirements.txt
cp default.env .env # modify your .env file!
sudo /bin/bash scripts/install.sh # will install and start+enable the systemd service for you
``` ```
## Support ## Support

46
api.py
View File

@ -1,46 +0,0 @@
#!/usr/bin/env python3
from flask import Flask
from headwind import Headwind as Headwind
app = Flask(__name__)
app.config.from_prefixed_env()
@app.route("/on")
async def on():
fan = Headwind(app, app.config["ADDRESS"])
fan_status = await fan.on()
if not fan_status:
return "<p>Failed to turn headwind on</p>", 503
return "<p>Turning headwind on</p>", 200
@app.route("/sleep")
async def sleep():
fan = Headwind(app, app.config["ADDRESS"])
fan_status = await fan.sleep()
if not fan_status:
return "<p>Failed to put headwind to sleep</p>", 503
return "<p>Putting headwind to sleep</p>", 200
@app.route("/speed/<int:speed>")
async def speed(speed):
fan = Headwind(app, app.config["ADDRESS"])
fan_status = await fan.manualSpeed(speed)
if not fan_status:
return f"<p>Failed to set headwind speed to {speed}</p>", 503
return f"<p>Setting headwind speed to {speed}</p>", 200
@app.route("/hr")
async def hr():
fan = Headwind(app, app.config["ADDRESS"])
fan_status = await fan.hr()
if not fan_status:
return "<p>Failed to set headwind to HR mode</p>", 503
return "<p>Setting headwind to HR mode</p>", 200
@app.route("/off")
async def off():
fan = Headwind(app, app.config["ADDRESS"])
fan_status = await fan.off()
if not fan_status:
return "<p>Failed to turn headwind off</p>", 503
return "<p>Turning headwind off</p>", 200

12
bluewind/__init__.py Normal file
View File

@ -0,0 +1,12 @@
# Initialise flask
from flask import Flask
app = Flask(__name__)
app.config['SERVER_NAME'] = '0.0.0.0:5000'
app.config.from_prefixed_env()
# Initialise the bluetooth stack
from bluewind import headwind
fan = headwind.Headwind(app, app.config["ADDRESS"])
# Load the views
from bluewind import views

View File

@ -11,6 +11,8 @@ MIN_SPEED = [0x2, 0x1]
HALF_SPEED = [0x2, 0x32] HALF_SPEED = [0x2, 0x32]
FULL_SPEED = [0x2, 0x64] FULL_SPEED = [0x2, 0x64]
CHARACTERISTIC = "a026e038-0a7d-4ab3-97fa-f1500f9feb8b" CHARACTERISTIC = "a026e038-0a7d-4ab3-97fa-f1500f9feb8b"
# > read the above characteristic 0xFD-01-XX-04 where XX is speed
# > bytearray(b'\xfd\x01\x00\x01')
class Headwind: class Headwind:
fanClient = None fanClient = None
@ -19,7 +21,23 @@ class Headwind:
self.flaskApp = flaskApp self.flaskApp = flaskApp
self.fanClient = BleakClient(address) self.fanClient = BleakClient(address)
async def sleep(self): async def readSpeed(self):
try:
async with self.fanClient as client:
result = await client.read_gatt_char(CHARACTERISTIC)
return result[2]
except Exception:
return 0
async def readMode(self):
try:
async with self.fanClient as client:
result = await client.read_gatt_char(CHARACTERISTIC)
return result[3] # Return state code, needs to figure out what each code means
except Exception:
return 0
async def writeSleep(self):
try: try:
async with self.fanClient as client: async with self.fanClient as client:
await client.write_gatt_char(CHARACTERISTIC, SLEEP) await client.write_gatt_char(CHARACTERISTIC, SLEEP)
@ -27,7 +45,7 @@ class Headwind:
except Exception as e: except Exception as e:
return False return False
async def on(self): async def writeOn(self):
try: try:
async with self.fanClient as client: async with self.fanClient as client:
await client.write_gatt_char(CHARACTERISTIC, ON) await client.write_gatt_char(CHARACTERISTIC, ON)
@ -35,7 +53,7 @@ class Headwind:
except Exception as e: except Exception as e:
return False return False
async def speedMode(self): async def writeSpeedMode(self):
try: try:
async with self.fanClient as client: async with self.fanClient as client:
await client.write_gatt_char(CHARACTERISTIC, SPD) await client.write_gatt_char(CHARACTERISTIC, SPD)
@ -43,7 +61,7 @@ class Headwind:
except Exception as e: except Exception as e:
return False return False
async def hrMode(self): async def writeHrMode(self):
try: try:
async with self.fanClient as client: async with self.fanClient as client:
await client.write_gatt_char(CHARACTERISTIC, HR) await client.write_gatt_char(CHARACTERISTIC, HR)
@ -51,7 +69,7 @@ class Headwind:
except Exception as e: except Exception as e:
return False return False
async def manualSpeed(self, speed): async def writeSpeed(self, speed):
if speed > 0: if speed > 0:
value = [0x2, speed] value = [0x2, speed]
try: try:
@ -60,10 +78,9 @@ class Headwind:
return True return True
except Exception as e: except Exception as e:
return False return False
else: return False
return False
async def off(self): async def writeOff(self):
try: try:
async with self.fanClient as client: async with self.fanClient as client:
await client.write_gatt_char(CHARACTERISTIC, OFF) await client.write_gatt_char(CHARACTERISTIC, OFF)

View File

@ -15,4 +15,16 @@ service001e = "a026ee0c-0a7d-4ab3-97fa-f1500f9feb8b" # Vendor
service001e_char001f = "a026e038-0a7d-4ab3-97fa-f1500f9feb8b" # Vendor specific service001e_char001f = "a026e038-0a7d-4ab3-97fa-f1500f9feb8b" # Vendor specific
service001e_char001f_desc0021 = "00002902-0000-1000-8000-00805f9b34fb" # Client Characteristic Configuration service001e_char001f_desc0021 = "00002902-0000-1000-8000-00805f9b34fb" # Client Characteristic Configuration
handle = 0x0000 handle = 0x0000
address = 'F2:B3:F7:6A:24:48' address = 'F2:B3:F7:6A:24:48'
# bytearray(b'\xfd\x01\x19\x04')
# 127.0.0.1 - - [10/Nov/2023 15:56:50] "GET /get/speed HTTP/1.1" 200 -
# 127.0.0.1 - - [10/Nov/2023 15:57:14] "GET /speed/32 HTTP/1.1" 200 -
# bytearray(b'\xfd\x01 \x04')
# 127.0.0.1 - - [10/Nov/2023 15:57:23] "GET /get/speed HTTP/1.1" 200 -
# 127.0.0.1 - - [10/Nov/2023 15:58:01] "GET /speed/50 HTTP/1.1" 200 -
# bytearray(b'\xfd\x012\x04')
# 127.0.0.1 - - [10/Nov/2023 15:58:14] "GET /get/speed HTTP/1.1" 200 -
# 127.0.0.1 - - [10/Nov/2023 15:58:37] "GET /speed/100 HTTP/1.1" 200 -
# bytearray(b'\xfd\x01d\x04')
# 127.0.0.1 - - [10/Nov/2023 15:58:45] "GET /get/speed HTTP/1.1" 200 -

80
bluewind/views.py Executable file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env python3
from bluewind import app, fan
@app.route("/on", methods=["POST"])
async def setOn():
fan_status = await fan.writeOn()
if not fan_status:
return "Failed to turn headwind on", 503
return "Turning headwind on", 200
@app.route("/sleep", methods=["POST"])
async def setSleep():
fan_status = await fan.writeSleep()
if not fan_status:
return "Failed to put headwind to sleep", 503
return "Putting headwind to sleep", 200
@app.route("/speed/<int:speed>", methods=["POST"])
async def setSpeed(speed):
fan_status = await fan.writeSpeed(speed)
if not fan_status:
return f"Failed to set headwind speed to {speed}", 503
return f"Setting headwind speed to {speed}", 200
@app.route("/speed/increase", methods=["POST"])
async def setIncreaseSpeed():
print("increase speed")
currentSpeed = await fan.readSpeed()
newSpeed = 0
if currentSpeed < 25:
newSpeed = 25
elif currentSpeed < 50:
newSpeed = 50
elif currentSpeed < 75:
newSpeed = 75
elif currentSpeed < 100:
newSpeed = 100
fan_status = await fan.writeSpeed(newSpeed)
if not fan_status:
return f"Failed to increase speed by 25%", 503
return f"Increased speed by 25%", 200
@app.route("/speed/decrease", methods=["POST"])
async def setDecreaseSpeed():
print("decrease speed")
currentSpeed = await fan.readSpeed()
newSpeed = 0
if currentSpeed == 100:
newSpeed = 75
elif currentSpeed >= 75:
newSpeed = 50
elif currentSpeed >= 50:
newSpeed = 25
elif currentSpeed > 25:
newSpeed = 25
elif currentSpeed <= 25:
newSpeed = 1
fan_status = await fan.writeSpeed(newSpeed)
if not fan_status:
return f"Failed to decrease speed by 25%", 503
return f"Decreased speed by 25%", 200
@app.route("/speed", methods=["GET"])
async def getSpeed():
speed = await fan.readSpeed()
return f"{speed}", 200
@app.route("/hr", methods=["POST"])
async def setHr():
fan_status = await fan.writeHr()
if not fan_status:
return "Failed to set headwind to HR mode", 503
return "Setting headwind to HR mode", 200
@app.route("/off", methods=["POST"])
async def setOff():
fan_status = await fan.off()
if not fan_status:
return "Failed to turn headwind off", 503
return "Turning headwind off", 200

View File

@ -1,12 +0,0 @@
class Configurinator:
address = ""
cmd = ""
speed = ""
def __init__(self):
pass
def load_config(self, address, cmd, speed):
self.address = address
self.cmd = cmd
self.speed = speed

View File

@ -1 +1,2 @@
FLASK_ADDRESS='F2:B3:F7:6A:24:48' FLASK_ADDRESS='F2:B3:F7:6A:24:48'
FLASK_URL='127.0.0.1:5000'

33
main.py
View File

@ -1,32 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import asyncio from bluewind import app
import click
from config import Configurinator as Config
from headwind import Headwind as Headwind
@click.command() if __name__ == '__main__':
@click.option('--address', default=None, help='headwind mac address') app.run(debug=False)
@click.option('--cmd', default=None, help='command to send')
@click.option('--speed', default=1, help='manual speed value, 1 to 100')
def main(address, cmd, speed):
conf = Config()
conf.load_config(address, cmd, speed)
asyncio.run(bluewind(conf))
async def bluewind(conf):
fan = Headwind(conf.address)
match conf.cmd:
case 'on':
print("turning fan on")
await fan.on()
case 'sleep':
print("putting fan to sleep")
await fan.sleep()
case 'manual':
print("setting fan speed")
await fan.speed(conf.speed)
print('stuff')
if __name__ == "__main__":
main()

31
scripts/install.sh Normal file
View File

@ -0,0 +1,31 @@
#!/bin/bash
# Export the environment variables
if [ ! -f ".env" ]; then
echo "The .env file does not exist, please make a copy of default.env into .env and adjust its contents before proceeding."
exit 1
fi
export $(cat .env)
# Create new systemd file
touch /etc/systemd/system/bluewind.service
# Fill in the systemd file
echo -e """[Unit]
Description=Flask Application
After=multi-user.target
[Service]
Type=simple
ExecStart=$(pwd)/.venv/bin/python3 $(pwd)/main.py
Restart=always
Environment="FLASK_ADDRESS=$(echo $FLASK_ADDRESS)"
Environment="FLASK_URL=$(echo $FLASK_URL)"
[Install]
WantedBy=multi-user.target
""" > /etc/systemd/system/bluewind.service
# Reload systemd unit, start and enable the service
systemctl daemon-reload
systemctl enable bluewind.service
systemctl start bluewind.service