diff --git a/.gitignore b/.gitignore
index a6b7ba4..5e49b42 100644
--- a/.gitignore
+++ b/.gitignore
@@ -158,7 +158,4 @@ cython_debug/
# 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
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
-
-bluewind
-bluewind/*
\ No newline at end of file
+#.idea/
\ No newline at end of file
diff --git a/README.md b/README.md
index 4e8e79e..0acd1e1 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,29 @@ Bluetooth control app for Wahoo Headwind.
## Usage
-To run the flask app:
+To run the flask app for development and testing:
```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='
Turning headwind on
", 200 - -@app.route("/sleep") -async def sleep(): - fan = Headwind(app, app.config["ADDRESS"]) - fan_status = await fan.sleep() - if not fan_status: - return "Failed to put headwind to sleep
", 503 - return "Putting headwind to sleep
", 200 - -@app.route("/speed/Failed to set headwind speed to {speed}
", 503 - return f"Setting headwind speed to {speed}
", 200 - -@app.route("/hr") -async def hr(): - fan = Headwind(app, app.config["ADDRESS"]) - fan_status = await fan.hr() - if not fan_status: - return "Failed to set headwind to HR mode
", 503 - return "Setting headwind to HR mode
", 200 - -@app.route("/off") -async def off(): - fan = Headwind(app, app.config["ADDRESS"]) - fan_status = await fan.off() - if not fan_status: - return "Failed to turn headwind off
", 503 - return "Turning headwind off
", 200 \ No newline at end of file diff --git a/bluewind/__init__.py b/bluewind/__init__.py new file mode 100644 index 0000000..79e0781 --- /dev/null +++ b/bluewind/__init__.py @@ -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 \ No newline at end of file diff --git a/headwind/__init__.py b/bluewind/headwind/__init__.py similarity index 66% rename from headwind/__init__.py rename to bluewind/headwind/__init__.py index a2cb260..19d955c 100644 --- a/headwind/__init__.py +++ b/bluewind/headwind/__init__.py @@ -11,6 +11,8 @@ MIN_SPEED = [0x2, 0x1] HALF_SPEED = [0x2, 0x32] FULL_SPEED = [0x2, 0x64] 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: fanClient = None @@ -19,7 +21,23 @@ class Headwind: self.flaskApp = flaskApp 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: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, SLEEP) @@ -27,7 +45,7 @@ class Headwind: except Exception as e: return False - async def on(self): + async def writeOn(self): try: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, ON) @@ -35,7 +53,7 @@ class Headwind: except Exception as e: return False - async def speedMode(self): + async def writeSpeedMode(self): try: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, SPD) @@ -43,7 +61,7 @@ class Headwind: except Exception as e: return False - async def hrMode(self): + async def writeHrMode(self): try: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, HR) @@ -51,7 +69,7 @@ class Headwind: except Exception as e: return False - async def manualSpeed(self, speed): + async def writeSpeed(self, speed): if speed > 0: value = [0x2, speed] try: @@ -60,10 +78,9 @@ class Headwind: return True except Exception as e: return False - else: - return False + return False - async def off(self): + async def writeOff(self): try: async with self.fanClient as client: await client.write_gatt_char(CHARACTERISTIC, OFF) diff --git a/headwind/spec.py b/bluewind/headwind/spec.py similarity index 70% rename from headwind/spec.py rename to bluewind/headwind/spec.py index 1b3ed14..aafc64c 100644 --- a/headwind/spec.py +++ b/bluewind/headwind/spec.py @@ -15,4 +15,16 @@ service001e = "a026ee0c-0a7d-4ab3-97fa-f1500f9feb8b" # Vendor service001e_char001f = "a026e038-0a7d-4ab3-97fa-f1500f9feb8b" # Vendor specific service001e_char001f_desc0021 = "00002902-0000-1000-8000-00805f9b34fb" # Client Characteristic Configuration handle = 0x0000 -address = 'F2:B3:F7:6A:24:48' \ No newline at end of file +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 - \ No newline at end of file diff --git a/bluewind/views.py b/bluewind/views.py new file mode 100755 index 0000000..bd14bc3 --- /dev/null +++ b/bluewind/views.py @@ -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/