From fb6f10891d02dc33912dbde84e0d6b7d3b4cc647 Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Fri, 1 Mar 2024 15:01:08 +0100 Subject: [PATCH] Bluetooth monitor and more --- .gitignore | 3 +- full.yml | 38 +++-- inventory.yml | 7 + roles/bluetooth-monitor/files/bleak3.py | 23 --- roles/bluetooth-monitor/files/bleak4.py | 38 ----- roles/bluetooth-monitor/files/bleak_final.py | 92 ----------- roles/bluetooth-monitor/files/bleak_test.py | 10 -- roles/bluetooth-monitor/files/bleak_test2.py | 58 ------- roles/bluetooth-monitor/files/config.yaml | 3 - roles/bluetooth-monitor/files/filter.cpp | 131 +++++++++++++++ roles/bluetooth-monitor/files/filter.py | 91 +++++++++++ roles/bluetooth-monitor/files/filtered | Bin 0 -> 26528 bytes .../files/my_btmonitor.service | 11 ++ roles/bluetooth-monitor/tasks/main.yml | 18 ++- .../templates/my_btmonitor.py | 149 ++++++++++++++++++ .../handlers/main.yml | 4 + .../tasks/main.yml | 7 + roles/pi-irserver/files/irserver.service | 2 +- roles/pi-squeezeserver/tasks/main.yml | 2 +- server.yml | 1 + working.yml | 20 +++ 21 files changed, 462 insertions(+), 246 deletions(-) delete mode 100644 roles/bluetooth-monitor/files/bleak3.py delete mode 100644 roles/bluetooth-monitor/files/bleak4.py delete mode 100644 roles/bluetooth-monitor/files/bleak_final.py delete mode 100644 roles/bluetooth-monitor/files/bleak_test.py delete mode 100644 roles/bluetooth-monitor/files/bleak_test2.py delete mode 100644 roles/bluetooth-monitor/files/config.yaml create mode 100644 roles/bluetooth-monitor/files/filter.cpp create mode 100644 roles/bluetooth-monitor/files/filter.py create mode 100755 roles/bluetooth-monitor/files/filtered create mode 100644 roles/bluetooth-monitor/files/my_btmonitor.service create mode 100644 roles/bluetooth-monitor/templates/my_btmonitor.py create mode 100644 roles/pi-disable-onboard-bluetooth/handlers/main.yml create mode 100644 roles/pi-disable-onboard-bluetooth/tasks/main.yml create mode 100644 working.yml diff --git a/.gitignore b/.gitignore index df283ac..e7ca621 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ ve_* *.img /music __pycache__ -/roles/pi-squeezeserver/backup \ No newline at end of file +/roles/pi-squeezeserver/backup +venv \ No newline at end of file diff --git a/full.yml b/full.yml index f6cf558..f435970 100644 --- a/full.yml +++ b/full.yml @@ -9,6 +9,7 @@ # - pi-lirc # - pi-sispmctl # + - hosts: musikserverwohnzimmeroben roles: - pi-standard-setup @@ -16,26 +17,29 @@ - pi-squeezelite-custom - pi-shairport - pi-irserver - - pi-dhtsensor + #- pi-dhtsensor - pi-squeezeserver +- hosts: kitchenpi + roles: + - pi-standard-setup + - pi-hifiberry-amp + - pi-squeezelite-custom + - pi-shairport + - pi-lirc + - pi-dhtsensor + - pi-disable-onboard-bluetooth + - bluetooth-monitor -#- hosts: kitchenpi -# roles: -# - pi-standard-setup -# - pi-hifiberry-amp -# - pi-squeezelite-custom -# - pi-shairport -# - pi-lirc -# - pi-dhtsensor - -#- hosts: bedroompi -# roles: -# - pi-standard-setup -# - pi-squeezelite-custom -# - pi-shairport -# - pi-lirc -# - pi-dhtsensor +- hosts: bedroompi + roles: + - pi-standard-setup + - pi-squeezelite-custom + - pi-shairport + - pi-lirc + - pi-dhtsensor + - pi-disable-onboard-bluetooth + - bluetooth-monitor #- hosts: octopi # roles: diff --git a/inventory.yml b/inventory.yml index 19ef6d8..d9e9948 100644 --- a/inventory.yml +++ b/inventory.yml @@ -40,6 +40,7 @@ all: sensor_room_name_ascii: wohnzimmeroben sensor_room_name: WohnzimmerOben hifiberry_overlay: hifiberry-dacplus + my_btmonitor_pl0: 68 # default is 73 - increase to make more sensitve (i.e. lower distances) musicmouse: squeezelite_name: MusicMouse shairport_name: MusicMouse @@ -52,6 +53,10 @@ all: sensor_room_name_ascii: testraum sensor_room_name: Test Raum heatingpi: + server: + sensor_room_name: Arbeitszimmer + sensor_room_name_ascii: arbeitszimmer + my_btmonitor_pl0: 78 vars: ansible_user: root ansible_python_interpreter: /usr/bin/python3 @@ -61,3 +66,5 @@ all: home_assistant_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkM2QxYjAwYjkxZjY0MWVhYjA4YmZhMDYwYTg3YjRhNyIsImlhdCI6MTcwNDI3MDU5MSwiZXhwIjoyMDE5NjMwNTkxfQ.dzvejgEQd9hf-Yftzd7NkR5pv76GaLFczeOy-a2pa1o configure_wifi: false wifi_ssid: BauerWLAN + my_btmonitor_mqtt_username: my_btmonitor + my_btmonitor_mqtt_password: 8aBIAC14jaKKbla diff --git a/roles/bluetooth-monitor/files/bleak3.py b/roles/bluetooth-monitor/files/bleak3.py deleted file mode 100644 index 1bfe7e6..0000000 --- a/roles/bluetooth-monitor/files/bleak3.py +++ /dev/null @@ -1,23 +0,0 @@ -import asyncio -from bleak import BleakScanner - - -async def main(): - stop_event = asyncio.Event() - - # TODO: add something that calls stop_event.set() - - def callback(device, advertising_data): - print("device", device.address, "advertising_data", advertising_data) - # TODO: do something with incoming data - pass - - async with BleakScanner(callback, scanning_mode="active") as scanner: - # Important! Wait for an event to trigger stop, otherwise scanner - # will stop immediately. - await stop_event.wait() - - # scanner stops when block exits - ... - -asyncio.run(main()) diff --git a/roles/bluetooth-monitor/files/bleak4.py b/roles/bluetooth-monitor/files/bleak4.py deleted file mode 100644 index 903df98..0000000 --- a/roles/bluetooth-monitor/files/bleak4.py +++ /dev/null @@ -1,38 +0,0 @@ -import asyncio -import logging -from bleak import BleakClient, BleakScanner -from bleak.assigned_numbers import AdvertisementDataType -from bleak.backends.bluezdbus.advertisement_monitor import OrPattern -from bleak.backends.bluezdbus.scanner import BlueZScannerArgs - -async def scan(): - args = BlueZScannerArgs( - or_patterns=[OrPattern(0, AdvertisementDataType.MANUFACTURER_SPECIFIC_DATA, b"\x10\x05")] - ) - - async with BleakScanner(bluez=args, scanning_mode="passive") as scanner: - async for _, advertisement_data in scanner.advertisement_data(): - mfr_data = advertisement_data.manufacturer_data - if mfr_data.get(0x02e1): - logging.info("scan(): found correct device: %s", mfr_data) - else: - logging.info("scan(): this should never happen: %s", mfr_data) - -async def connect(): - device1 = await BleakScanner.find_device_by_address("01:B6:EC:10:CB:8F") - - async with BleakClient(device1): - logging.info("connect(): connected to device") - await asyncio.sleep(60) - -async def main(): - logging.info("main(): starting scan") - asyncio.create_task(scan()) - await asyncio.sleep(30) - -logging.basicConfig( - level=logging.INFO, - format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s", -) - -asyncio.run(main()) diff --git a/roles/bluetooth-monitor/files/bleak_final.py b/roles/bluetooth-monitor/files/bleak_final.py deleted file mode 100644 index 3b65c2f..0000000 --- a/roles/bluetooth-monitor/files/bleak_final.py +++ /dev/null @@ -1,92 +0,0 @@ -import asyncio -from bleak import BleakScanner -from bleak.assigned_numbers import AdvertisementDataType -from bleak.backends.bluezdbus.advertisement_monitor import OrPattern -from bleak.backends.bluezdbus.scanner import BlueZScannerArgs -from Crypto.Cipher import AES -from typing import Dict - -irks = { - "aa67542b82c0e05d65c27fb7e313aba5": "martins_apple_watch", - "840e3892644c1ebd1594a9069c14ce0d" : "martins_iphone", -} - -def resolve_rpa(rpa: bytes, irk: bytes) -> bool: - """Compares the random address rpa to an irk (secret key) and return True if it matches""" - assert len(rpa) == 6 - assert len(irk) == 16 - - key = irk - plain_text = b'\x00' * 16 - plain_text = bytearray(plain_text) - plain_text[15] = rpa[3] - plain_text[14] = rpa[4] - plain_text[13] = rpa[5] - plain_text = bytes(plain_text) - - cipher = AES.new(key, AES.MODE_ECB) - cipher_text = cipher.encrypt(plain_text) - return cipher_text[15] == rpa[0] and cipher_text[14] == rpa[1] and cipher_text[13] == rpa[2] - - -def addr_to_bytes(addr:str) -> bytes: - """Converts a bluetooth mac address string with semicolons to bytes""" - str_without_colons = addr.replace(":", "") - bytearr = bytearray.fromhex(str_without_colons) - bytearr.reverse() - return bytes(bytearr) - - -def decode_address(addr: str, irks: Dict[str, str]): - """ - addr is a bluetooth address as a string e.g. 4d:24:12:12:34:10 - irks is dict with irk as a hex string, mapping to device name - """ - for irk, name in irks.items(): - if resolve_rpa(addr_to_bytes(addr), bytes.fromhex(irk)): - return name - return None - - -def estimate_distance(rssi, tx_power, pl0=54): - """ - RSSI in dBm - txPower is a transmitter parameter that calculated according to its physic layer and antenna in dBm - Return value in meter - - You should calculate "PL0" in calibration stage: - PL0 = txPower - RSSI; // When distance is distance0 (distance0 = 1m or more) - - SO, RSSI will be calculated by below formula: - RSSI = txPower - PL0 - 10 * n * log(distance/distance0) - G(t) - G(t) ~= 0 //This parameter is the main challenge in achiving to more accuracy. - n = 2 (Path Loss Exponent, in the free space is 2) - distance0 = 1 (m) - distance = 10 ^ ((txPower - RSSI - PL0 ) / (10 * n)) - - Read more details: - https://en.wikipedia.org/wiki/Log-distance_path_loss_model - """ - n = 2.7 - return 10**(( tx_power - rssi - pl0) / (10 * n)) - - -def callback(device, advertising_data): - print(device.address) - decoded = decode_address(device.address, irks) - if decoded: - print(f"{decoded} @ {advertising_data.rssi} distance {estimate_distance(advertising_data.rssi, advertising_data.tx_power)}") - - -async def main(): - # Scan for - args = BlueZScannerArgs( - or_patterns=[OrPattern(0, AdvertisementDataType.MANUFACTURER_SPECIFIC_DATA, b"\x10\x05")] - ) - stop_event = asyncio.Event() - - #async with BleakScanner(bluez=args, scanning_mode="passive") as scanner: - async with BleakScanner(callback) as scanner: - await stop_event.wait() - -asyncio.run(main()) diff --git a/roles/bluetooth-monitor/files/bleak_test.py b/roles/bluetooth-monitor/files/bleak_test.py deleted file mode 100644 index 6bb7b86..0000000 --- a/roles/bluetooth-monitor/files/bleak_test.py +++ /dev/null @@ -1,10 +0,0 @@ -import asyncio -from bleak import BleakScanner - -async def main(): - devices = await BleakScanner.discover() - while True: - for d in devices: - print(d) - -asyncio.run(main()) diff --git a/roles/bluetooth-monitor/files/bleak_test2.py b/roles/bluetooth-monitor/files/bleak_test2.py deleted file mode 100644 index 0d46da9..0000000 --- a/roles/bluetooth-monitor/files/bleak_test2.py +++ /dev/null @@ -1,58 +0,0 @@ -"""Scan for iBeacons. -Copyright (c) 2022 Koen Vervloesem -SPDX-License-Identifier: MIT -""" -import asyncio -from uuid import UUID - -from construct import Array, Byte, Const, Int8sl, Int16ub, Struct -from construct.core import ConstError - -from bleak import BleakScanner -from bleak.backends.device import BLEDevice -from bleak.backends.scanner import AdvertisementData - -ibeacon_format = Struct( - "type_length" / Const(b"\x02\x15"), - "uuid" / Array(16, Byte), - "major" / Int16ub, - "minor" / Int16ub, - "power" / Int8sl, -) - - -def device_found( - device: BLEDevice, advertisement_data: AdvertisementData -): - """Decode iBeacon.""" - try: - apple_data = advertisement_data.manufacturer_data[0x004C] - print("apple data", apple_data) - ibeacon = ibeacon_format.parse(apple_data) - uuid = UUID(bytes=bytes(ibeacon.uuid)) - print(f"UUID : {uuid}") - print(f"Major : {ibeacon.major}") - print(f"Minor : {ibeacon.minor}") - print(f"TX power : {ibeacon.power} dBm") - print(f"RSSI : {device.rssi} dBm") - print(47 * "-") - except KeyError: - # Apple company ID (0x004c) not found - pass - except ConstError: - # No iBeacon (type 0x02 and length 0x15) - pass - - -async def main(): - """Scan for devices.""" - scanner = BleakScanner() - scanner.register_detection_callback(device_found) - - while True: - await scanner.start() - await asyncio.sleep(1.0) - await scanner.stop() - - -asyncio.run(main()) diff --git a/roles/bluetooth-monitor/files/config.yaml b/roles/bluetooth-monitor/files/config.yaml deleted file mode 100644 index 6ef2d29..0000000 --- a/roles/bluetooth-monitor/files/config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -devices: - martins_iphone: 840e3892644c1ebd1594a9069c14ce0d - martins_apple_watch: aa67542b82c0e05d65c27fb7e313aba5 diff --git a/roles/bluetooth-monitor/files/filter.cpp b/roles/bluetooth-monitor/files/filter.cpp new file mode 100644 index 0000000..38d85b1 --- /dev/null +++ b/roles/bluetooth-monitor/files/filter.cpp @@ -0,0 +1,131 @@ +#include +#include +#include + +using real_t = double; + +static constexpr real_t SPIKE_THRESHOLD = 1.0f; // Threshold for spike detection +static constexpr int NUM_READINGS = 12; // Number of readings to keep track of + + +class FilteredDistance { + public: + FilteredDistance(real_t minCutoff = 1e-1f, real_t beta = 1e-3, real_t dcutoff = 5e-3); + void addMeasurement(real_t dist, real_t time_now_in_seconds); + const real_t getMedianDistance() const; + const real_t getDistance() const; + const real_t getVariance() const; + + bool hasValue() const { return lastTime != 0; } + + private: + real_t minCutoff; + real_t beta; + real_t dcutoff; + real_t x, dx; + real_t lastDist; + real_t lastTime; + + real_t getAlpha(real_t cutoff, real_t dT); + + real_t readings[NUM_READINGS]; // Array to store readings + int readIndex; // Current position in the array + real_t total; // Total of the readings + real_t totalSquared; // Total of the squared readings + + void initSpike(real_t dist); + real_t removeSpike(real_t dist); +}; + +FilteredDistance::FilteredDistance(real_t minCutoff, real_t beta, real_t dcutoff) + : minCutoff(minCutoff), beta(beta), dcutoff(dcutoff), x(0), dx(0), lastDist(0), lastTime(-1), total(0), totalSquared(0), readIndex(0) { +} + +void FilteredDistance::initSpike(real_t dist) { + for (size_t i = 0; i < NUM_READINGS; i++) { + readings[i] = dist; + } + total = dist * NUM_READINGS; + totalSquared = dist * dist * NUM_READINGS; // Initialize sum of squared distances +} + +real_t FilteredDistance::removeSpike(real_t dist) { + total -= readings[readIndex]; // Subtract the last reading + totalSquared -= readings[readIndex] * readings[readIndex]; // Subtract the square of the last reading + + readings[readIndex] = dist; // Read the sensor + total += readings[readIndex]; // Add the reading to the total + totalSquared += readings[readIndex] * readings[readIndex]; // Add the square of the reading + + readIndex = (readIndex + 1) % NUM_READINGS; // Advance to the next position in the array + + auto average = total / static_cast(NUM_READINGS); // Calculate the average + + if (std::fabs(dist - average) > SPIKE_THRESHOLD) + return average; // Spike detected, use the average as the filtered value + + return dist; // No spike, return the new value +} + +void FilteredDistance::addMeasurement(real_t dist, real_t time_now_in_seconds) { + const bool initialized = lastTime >= 0; + const real_t elapsed = time_now_in_seconds - lastTime; + lastTime = time_now_in_seconds; + + if (!initialized) { + x = dist; // Set initial filter state to the first reading + dx = 0; // Initial derivative is unknown, so we set it to zero + lastDist = dist; + initSpike(dist); + } else { + real_t dT = std::max(elapsed, real_t(0.05)); // Convert microseconds to seconds, enforce a minimum dT + const real_t alpha = getAlpha(minCutoff, dT); + const real_t dAlpha = getAlpha(dcutoff, dT); + dist = removeSpike(dist); + x += alpha * (dist - x); + dx = dAlpha * ((dist - lastDist) / dT); + lastDist = x + beta * dx; + std::cout << "alpha=" << alpha << + " dAlpha=" << dAlpha << + " dist=" << dist << + " x=" << x << + " dx=" << dx << + " lastDist=" << lastDist << + std::endl; + } +} + +const real_t FilteredDistance::getDistance() const { + return lastDist; +} + +real_t FilteredDistance::getAlpha(real_t cutoff, real_t dT) { + real_t tau = 1.0f / (2 * M_PI * cutoff); + return 1.0f / (1.0f + tau / dT); +} + +const real_t FilteredDistance::getVariance() const { + auto mean = total / static_cast(NUM_READINGS); + auto meanOfSquares = totalSquared / static_cast(NUM_READINGS); + auto variance = meanOfSquares - (mean * mean); // Variance formula: E(X^2) - (E(X))^2 + if (variance < 0.0f) return 0.0f; + return variance; +} + + +int main(int argc, char**argv) +{ + FilteredDistance f; + std::vector values = {1.5, 2.9, 5.3, 15.1, 1.5, 2.5, 1.5, 2.9, 5.3, 15.1}; + + real_t time = 0.0; + //std::cout << " result_cpp = ["; + for(int i=0; i < 1; ++i) + for(auto value : values) { + f.addMeasurement(value, time); + time += 1.0; + //std::cout << f.getDistance() << ", "; + } + //std::cout << "]" << std::endl; + return 0; +} \ No newline at end of file diff --git a/roles/bluetooth-monitor/files/filter.py b/roles/bluetooth-monitor/files/filter.py new file mode 100644 index 0000000..ae26bd8 --- /dev/null +++ b/roles/bluetooth-monitor/files/filter.py @@ -0,0 +1,91 @@ +#from time import time +import math +from scipy import signal + +# Taken from ESPresense C++ code +class FilteredDistance: + NUM_READINGS = 100 + SPIKE_THRESHOLD = 1.0 + + def __init__(self, min_cutoff : float = 1e-1, beta : float = 1e-3, dcutoff : float = 5e-3): + self.min_cutoff = min_cutoff + self.beta = beta + self.dcutoff = dcutoff + self.x = 0 + self.dx = 0 + self.last_dist = 0 + self.last_time = -1.0 + self.total = 0 + self.read_index = 0 + self.readings = [] + + def _init_spike(self, dist : float): + self.readings = [dist] * self.NUM_READINGS + self.total = sum(self.readings) + + def _remove_spike(self, dist: float): + self.total -= self.readings[self.read_index] + + self.readings[self.read_index] = dist + + self.total += dist + + self.read_index = (self.read_index + 1) % self.NUM_READINGS + average = self.total / self.NUM_READINGS + if abs(dist - average) > self.SPIKE_THRESHOLD: + return average # spike detected + else: + return dist + + def _get_alpha(self, cutoff : float, dT : float): + tau = 1 / (2 * math.pi * cutoff) + return 1 / (1 + tau / dT) + + def add_measurement(self, dist : float, time_now_in_seconds: float): + initialized = (self.last_time >= 0.0) + elapsed = time_now_in_seconds - self.last_time + self.last_time = time_now_in_seconds + if not initialized: + self.x = dist + self.dx = 0 + self.last_dist = dist + self._init_spike(dist) + else: + dT = max(elapsed, 0.05) + alpha = self._get_alpha(self.min_cutoff, dT) + d_alpha = self._get_alpha(self.dcutoff, dT) + dist = self._remove_spike(dist) + self.x += alpha * (dist - self.x) + self.dx = d_alpha * ((dist - self.last_dist) / dT) + self.last_dist = self.x + self.beta * self.dx + #print(f"{alpha=} {d_alpha=} {dist=} {self.x=} {self.dx=} {self.last_dist=}") + + def get_distance(self): + return self.last_dist + +def run_test(times, values, **kwargs): + f = FilteredDistance(**kwargs) + result = [] + for t, value in zip(times, values): + f.add_measurement(value, t) + result.append(f.get_distance()) + return result + +def smooth(y, box_pts): + box = np.ones(box_pts)/box_pts + y_smooth = np.convolve(y, box, mode='same') + return y_smooth + +if __name__ == "__main__": + import numpy as np + values = np.array([1] * 20 + [2, 4, 6, 7, 10, 16, 10, 13, 16, 24, 13] + [1] * 20 ) + + times = np.arange(0, len(values)) * 10 + result_default = run_test(times, values) + result_beta1 = smooth(values, 6) + import matplotlib.pyplot as plt + plt.plot(times, values, label="raw") + #plt.plot(times, result_default, marker="o", label="filtered") + plt.plot(times, result_beta1, marker='x', label="altered") + plt.legend() + plt.show() diff --git a/roles/bluetooth-monitor/files/filtered b/roles/bluetooth-monitor/files/filtered new file mode 100755 index 0000000000000000000000000000000000000000..2aaa9c8e9c4c9cd9b5cf852e95ce141697165cb5 GIT binary patch literal 26528 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 bool: + """Compares the random address rpa to an irk (secret key) and return True if it matches""" + assert len(rpa) == 6 + assert len(irk) == 16 + + key = irk + plain_text = b'\x00' * 16 + plain_text = bytearray(plain_text) + plain_text[15] = rpa[3] + plain_text[14] = rpa[4] + plain_text[13] = rpa[5] + plain_text = bytes(plain_text) + + cipher = AES.new(key, AES.MODE_ECB) + cipher_text = cipher.encrypt(plain_text) + return cipher_text[15] == rpa[0] and cipher_text[14] == rpa[1] and cipher_text[13] == rpa[2] + + +def addr_to_bytes(addr:str) -> bytes: + """Converts a bluetooth mac address string with semicolons to bytes""" + str_without_colons = addr.replace(":", "") + bytearr = bytearray.fromhex(str_without_colons) + bytearr.reverse() + return bytes(bytearr) + + +def decode_address(addr: str, irks: Dict[str, str]): + """ + addr is a bluetooth address as a string e.g. 4d:24:12:12:34:10 + irks is dict with irk as a hex string, mapping to device name + """ + for irk, name in irks.items(): + if resolve_rpa(addr_to_bytes(addr), bytes.fromhex(irk)): + return name + return None + + +def estimate_distance(rssi, tx_power, pl0=73): + """ + RSSI in dBm + txPower is a transmitter parameter that calculated according to its physic layer and antenna in dBm + Return value in meter + + You should calculate "PL0" in calibration stage: + PL0 = txPower - RSSI; // When distance is distance0 (distance0 = 1m or more) + + SO, RSSI will be calculated by below formula: + RSSI = txPower - PL0 - 10 * n * log(distance/distance0) - G(t) + G(t) ~= 0 //This parameter is the main challenge in achiving to more accuracy. + n = 2 (Path Loss Exponent, in the free space is 2) + distance0 = 1 (m) + distance = 10 ^ ((txPower - RSSI - PL0 ) / (10 * n)) + + Read more details: + https://en.wikipedia.org/wiki/Log-distance_path_loss_model + """ + n = 3.5 + return 10**((tx_power - rssi - pl0) / (10 * n)) + + +def smooth(y, box_pts): + box = np.ones(box_pts)/box_pts + y_smooth = np.convolve(y, box, mode='same') + return y_smooth + +WINDOW_SIZE = 6 +last_values = collections.deque(maxlen=WINDOW_SIZE) +def filter_distance(dist: float): + global last_values + last_values.append(dist) + return smooth(np.array(last_values), 12)[-1] + + +async def on_device_found_callback(irks, mqtt_client, room, device, advertising_data): + decoded_device_id = decode_address(device.address, irks) + rssi = advertising_data.rssi + tx_power = advertising_data.tx_power + if decoded_device_id and tx_power is not None and rssi is not None: + topic = f"my_btmonitor/devices/{decoded_device_id}/{room}" + distance = estimate_distance(rssi, tx_power, {{my_btmonitor_pl0 | default('73')}} ) + filtered_distance = filter_distance(distance) + data = {"id": decoded_device_id, + "name": decoded_device_id, + "rssi": rssi, + "tx_power": tx_power, + "distance": filtered_distance, + "unfiltered_distance": distance, + } + await mqtt_client.publish(topic, json.dumps(data).encode()) + #print(data) + + +async def main(): + stop_event = asyncio.Event() + + mqtt_conf = config['mqtt'] + while True: + try: + async with asyncio_mqtt.Client(hostname=mqtt_conf["hostname"], + username=mqtt_conf["username"], + password=mqtt_conf['password']) as mqtt_client: + cb = partial(on_device_found_callback, config['irk_to_devicename'], mqtt_client, mqtt_conf['room']) + active_scan = True + if active_scan: + async with BleakScanner(cb) as scanner: + await stop_event.wait() + else: + # Doesn't work, because of the strange or_patters + args = BlueZScannerArgs( + or_patterns=[OrPattern(0, AdvertisementDataType.MANUFACTURER_SPECIFIC_DATA, b"\x00\x4c")] + ) + async with BleakScanner(cb, bluez=args, scanning_mode="passive") as scanner: + await stop_event.wait() + except Exception as e: + print("Error", e) + print("Starting again") + +asyncio.run(main()) diff --git a/roles/pi-disable-onboard-bluetooth/handlers/main.yml b/roles/pi-disable-onboard-bluetooth/handlers/main.yml new file mode 100644 index 0000000..173576f --- /dev/null +++ b/roles/pi-disable-onboard-bluetooth/handlers/main.yml @@ -0,0 +1,4 @@ +--- +- name: reboot + reboot: + diff --git a/roles/pi-disable-onboard-bluetooth/tasks/main.yml b/roles/pi-disable-onboard-bluetooth/tasks/main.yml new file mode 100644 index 0000000..dcc4bbf --- /dev/null +++ b/roles/pi-disable-onboard-bluetooth/tasks/main.yml @@ -0,0 +1,7 @@ +--- +- name: Deactivate onboard bluetooth + lineinfile: + path: /boot/firmware/config.txt + regexp: "^#?dtoverlay=disable-bt" + line: "dtoverlay=disable-bt" + notify: reboot \ No newline at end of file diff --git a/roles/pi-irserver/files/irserver.service b/roles/pi-irserver/files/irserver.service index 6fc93c0..e1596c5 100644 --- a/roles/pi-irserver/files/irserver.service +++ b/roles/pi-irserver/files/irserver.service @@ -1,6 +1,6 @@ [Unit] Description=IR server for IR remotes -After=multi-user.target +After=network.target [Service] Type=simple diff --git a/roles/pi-squeezeserver/tasks/main.yml b/roles/pi-squeezeserver/tasks/main.yml index d0773fd..97bbbf7 100644 --- a/roles/pi-squeezeserver/tasks/main.yml +++ b/roles/pi-squeezeserver/tasks/main.yml @@ -6,7 +6,7 @@ - libflac-dev - libfaad2 - libmad0 - - perl-openssl-abi-1.1 + - perl-openssl-defaults - libnet-ssleay-perl - libio-socket-ssl-perl - nasm diff --git a/server.yml b/server.yml index 055a8cc..0400071 100644 --- a/server.yml +++ b/server.yml @@ -6,4 +6,5 @@ - server-exthdd-mount - server-nfs - server-link-aggregation + - bluetooth-monitor diff --git a/working.yml b/working.yml new file mode 100644 index 0000000..52dba27 --- /dev/null +++ b/working.yml @@ -0,0 +1,20 @@ +--- + +- hosts: server + roles: + - bluetooth-monitor + +- hosts: musikserverwohnzimmeroben + roles: + - bluetooth-monitor + +- hosts: kitchenpi + roles: + - pi-disable-onboard-bluetooth + - bluetooth-monitor + +- hosts: bedroompi + roles: + - pi-disable-onboard-bluetooth + - bluetooth-monitor +