#!/usr/bin/env python3
#
# Copyright 2009 Joshua Wright, Michael Ossmann
# 
# This file is part of gr-bluetooth
# 
# gr-bluetooth 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 2, or (at your option)
# any later version.
# 
# gr-bluetooth 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 gr-bluetooth; see the file COPYING.  If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.

import sys
import struct
import time
from pcapdump.pcapdump import *

DLT_EN10MB = 1
DLT_BLUETOOTH_HCI_H4 = 187
ELLISYS_CSV_HDR = "\"Depth\",\"Time\",\"Name\",\"Data\"\x0d\x0a"
ELLISYS_HID_INPUT = "HID Input 1"
USBHID_MAP = {
    0x04 : "a",
    0x05 : "b",
    0x06 : "c",
    0x07 : "d",
    0x08 : "e",
    0x09 : "f",
    0x0A : "g",
    0x0B : "h",
    0x0C : "i",
    0x0D : "j",
    0x0E : "k",
    0x0F : "l",
    0x10 : "m",
    0x11 : "n",
    0x12 : "o",
    0x13 : "p",
    0x14 : "q",
    0x15 : "r",
    0x16 : "s",
    0x17 : "t",
    0x18 : "u",
    0x19 : "v",
    0x1A : "w",
    0x1B : "x",
    0x1C : "y",
    0x1D : "z",
    0x1E : "1",
    0x1F : "2",
    0x20 : "3",
    0x21 : "4",
    0x22 : "5",
    0x23 : "6",
    0x24 : "7",
    0x25 : "8",
    0x26 : "9",
    0x27 : "0",
    0x28 : "[Return]\n",
    0x29 : "[Esc]",
    0x2A : "[Backspace]",
    0x2B : "[Tab]\t",
    0x2C : " ",
    0x2D : "-",
    0x2E : "=",
    0x2F : "[",
    0x30 : "]",
    0x31 : "\\",
    0x32 : "#",
    0x33 : ";",
    0x34 : "'",
    0x35 : "[Grave Accent]",
    0x36 : ",",
    0x37 : ".",
    0x38 : "/",
    0x39 : "[Caps Lock]",
    0x3A : "[F1]",
    0x3B : "[F2]",
    0x3C : "[F3]",
    0x3D : "[F4]",
    0x3E : "[F5]",
    0x3F : "[F6]",
    0x40 : "[F7]",
    0x41 : "[F8]",
    0x42 : "[F9]",
    0x43 : "[F10]",
    0x44 : "[F11]",
    0x45 : "[F12]",
    0x46 : "[PrintScreen]",
    0x47 : "[Scroll]",
    0x48 : "[Pause]",
    0x49 : "[Insert]",
    0x4A : "[Home]",
    0x4B : "[PageUp]",
    0x4C : "[Delete]",
    0x4D : "[End]",
    0x4E : "[PageDown]",
    0x4F : "[RightArrow]",
    0x50 : "[LeftArrow]",
    0x51 : "[DownArrow]",
    0x52 : "[UpArrow]",
    0x53 : "[Keypad Num Lock and Clear]",
    0x54 : "[Keypad /]",
    0x55 : "[Keypad *]",
    0x56 : "[Keypad -]",
    0x57 : "[Keypad +]",
    0x58 : "[Keypad Enter]\n",
    0x59 : "[Keypad 1 and End]",
    0x5A : "[Keypad 2 and Down Arrow]",
    0x5B : "[Keypad 3 and PageDn]",
    0x5C : "[Keypad 4 and Left Arrow]",
    0x5D : "[Keypad 5]",
    0x5E : "[Keypad 6 and Right Arrow]",
    0x5F : "[Keypad 7 and Home]",
    0x60 : "[Keypad 8 and Up Arrow]",
    0x61 : "[Keypad 9 and PageUp]",
    0x62 : "[Keypad 0 and Insert]",
    0x63 : "[Keypad . and Delete]",
    0x64 : "\\",
    0x65 : "[WinKey]",
    0x66 : "[Power9]",
    0x67 : "[Keypad =]",
    0x68 : "[F13]",
    0x69 : "[F14]",
    0x6A : "[F15]",
    0x6B : "[F16]",
    0x6C : "[F17]",
    0x6D : "[F18]",
    0x6E : "[F19]",
    0x6F : "[F20]",
    0x70 : "[F21]",
    0x71 : "[F22]",
    0x72 : "[F23]",
    0x73 : "[F24]",
    0x74 : "[Execute]",
    0x75 : "[Help]",
    0x76 : "[Menu]",
    0x77 : "[Select]",
    0x78 : "[Stop]",
    0x79 : "[Again]",
    0x7A : "[Undo]",
    0x7B : "[Cut]",
    0x7C : "[Copy]",
    0x7D : "[Paste]",
    0x7E : "[Find]",
    0x7F : "[Mute]",
    0x80 : "[Volume Up]",
    0x81 : "[Volume Down]",
    0x82 : "[Locking Caps Lock]",
    0x83 : "[Locking Num Lock]",
    0x84 : "[Locking Scroll Lock]",
    0x85 : "[Keypad Comma]",
    0x86 : "[Keypad Equal]",
    0x87 : "[International1]",
    0x88 : "[International2]",
    0x89 : "[International3]",
    0x8A : "[International4]",
    0x8B : "[International5]",
    0x8C : "[International6]",
    0x8D : "[International7]",
    0x8E : "[International8]",
    0x8F : "[International9]",
    0x90 : "[LANG1]",
    0x91 : "[LANG2]",
    0x92 : "[LANG3]",
    0x93 : "[LANG4]",
    0x94 : "[LANG5]",
    0x95 : "[LANG6]",
    0x96 : "[LANG7]",
    0x97 : "[LANG8]",
    0x98 : "[LANG9]",
    0x99 : "[Alternate Erase]",
    0x9A : "[SysReq/Attention]",
    0x9B : "[Cancel]",
    0x9C : "[Clear]",
    0x9D : "[Prior]",
    0x9E : "[Return]\n",
    0x9F : "[Separator]",
    0xA0 : "[Out]",
    0xA1 : "[Oper]",
    0xA2 : "[Clear/Again]",
    0xA3 : "[CrSel/Props]",
    0xA4 : "[ExSel]",
    0xB0 : "[Keypad 00]",
    0xB1 : "[Keypad 000]",
    0xB2 : "[Thousands Separator]",
    0xB3 : "[Decimal Separator]",
    0xB4 : "[Currency Unit]",
    0xB5 : "[Currency Sub-unit]",
    0xB6 : "[Keypad (]",
    0xB7 : "[Keypad )]",
    0xB8 : "[Keypad {]",
    0xB9 : "[Keypad }]",
    0xBA : "[Keypad Tab]\t",
    0xBB : "[Keypad Backspace]",
    0xBC : "[Keypad A]",
    0xBD : "[Keypad B]",
    0xBE : "[Keypad C]",
    0xBF : "[Keypad D]",
    0xC0 : "[Keypad E]",
    0xC1 : "[Keypad F]",
    0xC2 : "[Keypad XOR]",
    0xC3 : "[Keypad ^]",
    0xC4 : "[Keypad %]",
    0xC5 : "[Keypad <]",
    0xC6 : "[Keypad >]",
    0xC7 : "[Keypad &]",
    0xC8 : "[Keypad &&]",
    0xC9 : "[Keypad |]",
    0xCA : "[Keypad ||]",
    0xCB : "[Keypad :]",
    0xCC : "[Keypad #]",
    0xCD : "[Keypad Space]",
    0xCE : "[Keypad @]",
    0xCF : "[Keypad !]",
    0xD0 : "[Keypad Memory Store]",
    0xD1 : "[Keypad Memory Recall]",
    0xD2 : "[Keypad Memory Clear]",
    0xD3 : "[Keypad Memory Add]",
    0xD4 : "[Keypad Memory Subtract]",
    0xD5 : "[Keypad Memory Multiply]",
    0xD6 : "[Keypad Memory Divide]",
    0xD7 : "[Keypad +/-]",
    0xD8 : "[Keypad Clear]",
    0xD9 : "[Keypad Clear Entry]",
    0xDA : "[Keypad Binary]",
    0xDB : "[Keypad Octal]",
    0xDC : "[Keypad Decimal]",
    0xDD : "[Keypad Hexadecimal]",
    0xE0 : "[LeftControl]",
    0xE1 : "[LeftShift]",
    0xE2 : "[LeftAlt]",
    0xE3 : "[LeftWinKey]",
    0xE4 : "[RightControl]",
    0xE5 : "[RightShift]",
    0xE6 : "[RightAlt]",
    0xE7 : "[RightWinKey]"
}

# some keycodes represent different things when Shift is held down
USBHID_SHIFT_MAP = {
    0x04 : "A",
    0x05 : "B",
    0x06 : "C",
    0x07 : "D",
    0x08 : "E",
    0x09 : "F",
    0x0A : "G",
    0x0B : "H",
    0x0C : "I",
    0x0D : "J",
    0x0E : "K",
    0x0F : "L",
    0x10 : "M",
    0x11 : "N",
    0x12 : "O",
    0x13 : "P",
    0x14 : "Q",
    0x15 : "R",
    0x16 : "S",
    0x17 : "T",
    0x18 : "U",
    0x19 : "V",
    0x1A : "W",
    0x1B : "X",
    0x1C : "Y",
    0x1D : "Z",
    0x1E : "!",
    0x1F : "@",
    0x20 : "#",
    0x21 : "$",
    0x22 : "%",
    0x23 : "^",
    0x24 : "&",
    0x25 : "*",
    0x26 : "(",
    0x27 : ")",
    0x2D : "_",
    0x2E : "+",
    0x2F : "{",
    0x30 : "}",
    0x31 : "|",
    0x32 : "~",
    0x33 : ":",
    0x34 : "\"",
    0x35 : "~",
    0x36 : "<",
    0x37 : ">",
    0x38 : "?",
    0x64 : "|"
}

# global variable to track currently depressed keys
active_keys = []

def hid2ascii(scancode, shift):
    '''
    Convert the specified scancode value to the ASCII equivalent using the
    USBHID_MAP list.
    '''
    if shift:
        try:
            code = USBHID_SHIFT_MAP[scancode]
            return code
        except KeyError:
            pass
    try:
        code = USBHID_MAP[scancode]
    except KeyError:
        return "[Reserved]" 
    return code

def usage():
    print("Usage: btaptap [-r pcapfile.pcap | -e ellisysfile.csv] [-c count] [-h]\n", file=sys.stderr)
    sys.exit(0)

def parse_l2cap_keydata(l2cappkt):
    global active_keys

    TRANS_HDR_IN_DATA = 0xA1
    REPORT_ID_KEYBOARD = 0x01
    CTRL = 1
    SHIFT = 2
    ALT = 4
    GUI = 8

    # Keyboard keystrokes are only seen in L2CAP packets at least 10 bytes long
    l2clen = (ord(l2cappkt[1]) << 8) | ord(l2cappkt[0])
    if l2clen < 10:
        return

    # Keyboard keystrokes are only carried by Channel ID >= 0x40
    cid = (ord(l2cappkt[3]) << 8) | ord(l2cappkt[2])
    if cid < 0x40:
        return
    # Ideally we would check for the particular CID for the HID_INTERRUPT
    # channel, but we don't handle the negotiation (and may not have even
    # seen it).

    # Transaction Header should indicate input data
    thdr = ord(l2cappkt[4])
    if thdr != TRANS_HDR_IN_DATA:
        return

    # Report ID should indicate this is a keyboard
    rid = ord(l2cappkt[5])
    if rid != REPORT_ID_KEYBOARD:
        return

    # This byte describes modifier key status (one bit per key)
    mod = ord(l2cappkt[6])

    # We don't care whether left or right modifier keys are pressed, so we
    # combine the status bits.
    leftmod = mod & 0x0f
    rightmod = (mod & 0xf0) >> 4
    mod = leftmod | rightmod

    # up to six keys can be reported at once
    keycodes = []
    #for byte in range(8,14):
    for byte in range(8,11):
        keystroke = ord(l2cappkt[byte])
        if keystroke != 0x00:
            keycodes.append(keystroke)
    
    for keystroke in keycodes:
        # don't repeat keys that are still held down
        if active_keys.count(keystroke) == 0:

            if (mod & CTRL):
                sys.stdout.write("CTRL^")
            if (mod & ALT):
                sys.stdout.write("ALT^")
            if (mod & GUI): # e.g. Windows key
                sys.stdout.write("GUI^")

            sys.stdout.write(hid2ascii(keystroke, mod & SHIFT))

    active_keys = keycodes

def parse_ellisys_export(exportfile):
    try:
        cap = open(exportfile, "r")
    except (OSError, IOError) as e:
        print("Unable to open Ellisys capture file.", file=sys.stderr)
        return

    # Check to make sure the CSV file header matches out expectations
    hdr=cap.readline()
    if (hdr != ELLISYS_CSV_HDR):
        print("Invalid CSV file (does not match Ellisys export format)", file=sys.stderr)
        return

    for packetline in cap.xreadlines():
        try:
            (edepth, etime, ename, edata) = packetline.replace('"', '').strip().split(",")
        except ValueError:
            continue

        # We are only interedted in HID Input data
        if (ename != ELLISYS_HID_INPUT): continue

        parse_ellisys_keydata(edata)

def parse_ellisys_keydata(payload):
    TRANS_HDR_IN_DATA = "\xA1"
    DEST_CHANNEL_ID = "\x06\x03"

    # Convert space-separated hex into string
    payload = payload.replace(' ','').decode("hex")

    # The Ellisys CSV file format doesn't give us the L2CAP data, so we fake it here by adding
    # a 2-byte length field, destination CID, and transaction header
    packet = chr(len(payload)+1) + "\x00" + DEST_CHANNEL_ID + TRANS_HDR_IN_DATA + payload
    parse_l2cap_keydata(packet)

def parse_bb_keydata(packet):

    BTBBHDR_TYPE_MASK = 0x78
    BTBBHDR_TYPE_SHIFT = 3
    BTBBHDR_TYPE_DM1 = 3
    BTBBPAYLOADHDR_LLID_MASK = 0x03
    BTBBPAYLOADHDR_LLID_SHIFT = 0
    BTBBPAYLOADHDR_LEN_MASK = 0xF8
    BTBBPAYLOADHDR_LEN_SHIFT = 3
    LLID_L2CAP = 2

    # Keyboard keystrokes are only seen in frames at least 40 bytes long
    if len(packet) < 40:
        return

    # Keyboard keystrokes are only seen in DM1 frames
    btbbhdr = packet[20:23]
    type = (ord(btbbhdr[0]) & BTBBHDR_TYPE_MASK) >> BTBBHDR_TYPE_SHIFT
    if type != BTBBHDR_TYPE_DM1:
        return

    # Keyboard keystrokes are only seen in L2CAP packets 14 bytes long
    btbbpayloadhdr = ord(packet[23])
    llid = btbbpayloadhdr & (BTBBPAYLOADHDR_LLID_MASK) >> BTBBPAYLOADHDR_LLID_SHIFT
    l2clen = (btbbpayloadhdr & BTBBPAYLOADHDR_LEN_MASK) >> BTBBPAYLOADHDR_LEN_SHIFT
    #print("Debug btbbpayloadhdr 0x%02x, llid %d, l2clen %d"%(btbbpayloadhdr, llid, l2clen))
    if llid != LLID_L2CAP or l2clen < 14:
        return

    parse_l2cap_keydata(packet[24:38])

def parse_hci_keydata(packet):

    HCI_TYPE_ACL_DATA = 2

    # Keyboard keystrokes are only seen in frames at least 19 bytes long
    if len(packet) < 19:
        return

    # Keyboard keystrokes are only seen in ACL Data frames
    type = ord(packet[0])
    if type != HCI_TYPE_ACL_DATA:
        return

    parse_l2cap_keydata(packet[5:])

if __name__ == '__main__':

    arg_pcapfile = None
    arg_ellisysfile = None
    arg_count = -1
    packetcount = 0

    while len(sys.argv) > 1:
        op = sys.argv.pop(1)
        if op == '-r':
            arg_pcapfile = sys.argv.pop(1)
        if op == '-c':
            arg_count = int(sys.argv.pop(1))
        if op == '-e':
            arg_ellisysfile = sys.argv.pop(1)
        if op == '-h':
            usage()
            sys.exit(0)

    if (arg_ellisysfile == None and arg_pcapfile == None):
        print("Must specify a libpcap capture or an Ellisys CSV file", file=sys.stderr)
        usage()
        sys.exit(0)

    if arg_pcapfile != None:
        cap = PcapReader(arg_pcapfile)
    
        while arg_count != packetcount:
            try:
                (pheader, packet) = cap.pnext()
                pkttime = pheader[0]
                packetcount+=1
        
                if cap.datalink() == DLT_EN10MB:
                    parse_bb_keydata(packet)
                elif cap.datalink() == DLT_BLUETOOTH_HCI_H4:
                    parse_hci_keydata(packet)
                else:
                    print("Unsupported libpcap data link layer: %d\n" % cap.datalink(), file=sys.stderr)
            except TypeError: # raised when pnext returns Null (end of capture)
                break
    
        cap.close()

    if arg_ellisysfile != None:
        parse_ellisys_export(arg_ellisysfile)

    print
