#!/usr/bin/env python3
#
# BTZen - Bluetooth Smart sensor reading library.
#
# Copyright (C) 2015-2017 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/>.
#

"""
Connect to OSTC dive computer and show last five dives.
"""

import argparse
import asyncio
import logging
import struct
import time
import sys
from collections import namedtuple
from cytoolz import itertoolz as itz
from datetime import datetime

import btzen._btzen as cbtzen
from btzen import Serial

logger = logging.getLogger(__name__)

DiveHeader = namedtuple('DiveHeader', ['start', 'size', 'datetime', 'end'])
to_int = lambda value: sum(v << (s * 8) for v, s in zip(value, range(len(value))))
to_timestamp = lambda value: datetime(value[0] + 2000, *value[1:])
to_self = lambda value: value
to_depth = lambda v: v * 9.80665 / 1000
to_duration = lambda v: (to_int(v[:2]) * 60 + int(v[2])) / 60
header_parsers = (to_self, to_int, to_timestamp, to_self)

def parse_header(item):
    fmt = '<H7x3s5s237xH'
    assert struct.calcsize(fmt) == 256
    item = struct.unpack(fmt, item)
    values = zip(header_parsers, item)
    values = (p(v) for p, v in values)
    return DiveHeader._make(values)

async def start(dev):
    await dev.write(b'\xbb')
    value = await dev.read(2)
    assert value == b'\xbb\x4d'

async def stop(dev):
    await dev.write(b'\xff')
    logger.debug('closing ostc connection')

async def display(dev, msg):
    msg = '{:16.16}'.format(msg).encode()

    await dev.write(b'\x6e')
    value = await dev.read(1)
    assert value == b'\x6e'
    await dev.write(msg)
    value = await dev.read(1)
    assert value == b'\x4d'

async def read_data(mac):
    dev = Serial(mac)
    await dev.connect()

    await start(dev)
    await display(dev, 'BTZen: headers')

    try:
        total = 65536 + 2
        start_at = time.monotonic()
        print('fetching headers...', end='')
        sys.stdout.flush()
        await dev.write(b'\x61')
        headers = await dev.read(total)
        headers = headers[1:-1]
        print('done at {:.1f}KB/s'.format(total / (time.monotonic() - start_at) / 1024))

        # get 5 latests dives
        items = itz.partition(256, headers)
        # convert back to bytes, see https://github.com/pytoolz/cytoolz/issues/102
        # also filter unused headers
        items = (bytes(v) for v in items if v[8] != 0xff)
        items = [parse_header(v) for v in items]

        count = len(items)

        for k, header in enumerate(items):
            assert header.start == 0xfafa and header.end == 0xfbfb

            await display(dev, 'BTZen: dive {}'.format(k))

            print('dive {}/{}, read {} bytes'.format(k, count - 1, 256 + header.size))

            # follow ostc protocol stricly, get echo byte first then send
            # the dive number or occasionally transfer will hang
            await dev.write(b'\x66')
            value = await dev.read(1)
            assert value == b'\x66'

            await dev.write(bytes([k]))
            data = await dev.read(256 + header.size - 2)

            profile_head = data[:256]
            profile = data[256:-1]
            marker = data[-1] # 0x4d byte

            total += len(data)

            assert profile_head[:2] == b'\xfa\xfa', profile_head[:2]
            assert profile_head[-2:] == b'\xfb\xfb', profile_head[-2:]
            assert header.size == to_int(profile[:3])
            assert profile[-2:] == b'\xfd\xfd', profile[-2:]
            assert marker == 0x4d, marker

        await display(dev, 'BTZen: done')
        print('done, {} bytes read, time {:.1f}s'.format(total, time.monotonic() - start_at))
    finally:
        await stop(dev)
        dev.close()

parser = argparse.ArgumentParser()
parser.add_argument(
    '--verbose', default=False, action='store_true',
    help='show debug log'
)
parser.add_argument('device', help='MAC address of device')
args = parser.parse_args()

if args.verbose:
    logging.basicConfig(level=logging.DEBUG)

loop = asyncio.get_event_loop()
loop.run_until_complete(read_data(args.device))

# vim: sw=4:et:ai
