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 0000000..2aaa9c8 Binary files /dev/null and b/roles/bluetooth-monitor/files/filtered differ diff --git a/roles/bluetooth-monitor/files/my_btmonitor.service b/roles/bluetooth-monitor/files/my_btmonitor.service new file mode 100644 index 0000000..3bf193c --- /dev/null +++ b/roles/bluetooth-monitor/files/my_btmonitor.service @@ -0,0 +1,11 @@ +[Unit] +Description=My Bluetooth monitor +After=network.target + +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/my_btmonitor + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/roles/bluetooth-monitor/tasks/main.yml b/roles/bluetooth-monitor/tasks/main.yml index 1ec0032..12355fe 100644 --- a/roles/bluetooth-monitor/tasks/main.yml +++ b/roles/bluetooth-monitor/tasks/main.yml @@ -1,8 +1,22 @@ -- name: Apt install bluez and firmware +- name: Apt install bluez, firmware and Python requirements apt: name: - bluez - bluez-firmware - firmware-realtek - firmware-realtek-rtl8723cs-bt - \ No newline at end of file + - python3-pycryptodome + - python3-bleak + - python3-asyncio-mqtt + - python3-numpy +- name: Copy monitor script + template: src=my_btmonitor.py dest=/usr/bin/my_btmonitor owner=root mode=u+rwx +- name: Install systemd service file + copy: src=my_btmonitor.service dest=/etc/systemd/system/ +- name: Add script to autostart and start now + systemd: name=my_btmonitor state=restarted enabled=yes daemon_reload=yes +#- name: Add to sysdweb +# include_role: +# name: pi-sysdweb +# vars: +# sysdweb_name: my_btmonitor \ No newline at end of file diff --git a/roles/bluetooth-monitor/templates/my_btmonitor.py b/roles/bluetooth-monitor/templates/my_btmonitor.py new file mode 100644 index 0000000..be49ce6 --- /dev/null +++ b/roles/bluetooth-monitor/templates/my_btmonitor.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +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 Cryptodome.Cipher import AES +from functools import partial +import asyncio_mqtt +import json +from typing import Dict +import collections +import numpy as np + +# ------------------- Config ---------------------------------------------------------------- + +config = { + "mqtt": { + "hostname": "homeassistant.fritz.box", + "username": "{{my_btmonitor_mqtt_username}}", + "password": "{{my_btmonitor_mqtt_password}}", + "room": "{{sensor_room_name_ascii}}" + }, + "irk_to_devicename": { + "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=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 +