#!/usr/bin/env python3
#
# BTZen - library to asynchronously access Bluetooth devices.
#
# Copyright (C) 2015-2022 by Artur Wroblewski <wrobell@riseup.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

"""
BTZen script to test cancellation of sensor data read requests.

The script uses SensorTag Bluetooth device.

Prerequisites

1. Change D-Bus configuration to use 128 maximum replies per connection in
   `/usr/share/dbus-1/session.conf` file::

        <limit name="max_replies_per_connection">128</limit>

2. Set Bluetooth minimum and maximum connection interval in
   `/etc/bluetooth/main.conf` file::

        MinConnectionInterval=40
        MaxConnectionInterval=60

3. Restart system.
4. Ensure Bluetooth service is started.
5. Ensure adapter is powered up.
6. Ensure the battery of SensorTag tag is fresh or connect it to
   non-battery power source.

Run the script::

    btzen-cancel 24:XX:XX:XX:XX:XX

The output of the script should be::

    [100917, 24.75, 58.53271484375, 592.96]
    sensor read cancelled, sleep for 5s to let d-bus breath
    ...
    [100909, 23.90625, 53.253173828125, 569.12]
    num timeouts 10000
    sensor read cancelled, sleep for 5s to let d-bus breath
    ...
    [100914, 23.5625, 53.326416015625, 555.04]
    num timeouts 20000
    sensor read cancelled, sleep for 5s to let d-bus breath
    ...

As root run::

    journalctl -f

and observe D-Bus issueing warnings about number of pending replies::

    dbus-daemon[511]: [system] The maximum number of pending replies for ":1.90" (uid=1000 pid=9983 comm="btzen-cancel 24:71:89:BE:E9:04 -") has been reached (max_replies_per_connection=1024)

Leave the script running for 24 hours.

The test is successful when it still outputs sensor values.
"""

import argparse
import asyncio
import logging
import typing as tp
import uvloop
from datetime import datetime
from collections.abc import Sequence

import btzen
import btzen.sensortag

logger = logging.getLogger()

async def read_sensors(
        sensors: Sequence[btzen.Device[btzen.Service, float]], timeout: float
    ) -> None:

    timeouts = 0
    attempts = 0
    while btzen.is_active():
        attempts += 1
        try:
            tasks = [asyncio.create_task(btzen.read(s)) for s in sensors]
            task = asyncio.gather(*tasks)

            # periodically show sensor values to ensure a tester that
            # sensor data reads are against a connected device
            if attempts == 1 or attempts % 10000 == 0:
                result = await task
            else:
                result = await asyncio.wait_for(task, timeout=timeout)
            print(result)
        except asyncio.CancelledError as ex:
            # timeout, no buffer space and connection errors are handled
            # by BTZen and raised as cancellations; it is up to a caller
            # to decide what to do
            print('sensor read cancelled, sleep for 5s to let d-bus breath', ex)
            await asyncio.sleep(5)
        except asyncio.TimeoutError as ex:
            # most of the time timeout from asyncio loop is expected,
            # count the timeouts and report them to see the script
            # progressing
            timeouts += 1
            if timeouts % 10000 == 0:
                print('num timeouts', timeouts)

async def start_test(iface: str, mac: str, timeout: float) -> None:
    ctor = [btzen.pressure, btzen.temperature, btzen.humidity, btzen.light]
    sensors: Sequence[btzen.Device[btzen.Service, float]] = [
        c(mac, make=btzen.Make.SENSOR_TAG) for c in ctor
    ]

    async with btzen.connect(sensors, interface=iface) as session:
        await asyncio.gather(session, read_sensors(sensors, timeout))

parser = argparse.ArgumentParser()
parser.add_argument(
    '--verbose', default=False, action='store_true',
    help='show debug log'
)
parser.add_argument(
    '-t', '--timeout', default=0.001, type=float,
    help='Sensor read timeout'
)
parser.add_argument(
    '-i', '--interface', default='hci0',
    help='Host controller interface (HCI)'
)
parser.add_argument('device', help='MAC address of device')
args = parser.parse_args()

level = logging.DEBUG if args.verbose else logging.WARNING
logging.basicConfig(level=level)

uvloop.install()
asyncio.run(start_test(args.interface, args.device, args.timeout))

# vim: sw=4:et:ai
