# (c) Copyright 2018 by Coinkite Inc. This file is part of Coldcard <coldcardwallet.com>
# and is covered by GPLv3 license found in COPYING.
#
# touchpad.py - Numeric keypad. Touch button matrix. Mark 1 only.
#
import array, utime
from uasyncio.queues import Queue
from machine import Pin
from random import shuffle
from numpad import NumpadBase
import touch

_singleton = None

NUM_PINS = const(7)

# The critical "threshold" .. remember, values below this
# might be "light" touches or proximity. 
THRESHOLD = const(200)

class TouchNumpad(NumpadBase):
    # (row, col) => keycode
    DECODER = {
            (3,2): '1',
            (3,1): '2',
            (3,0): '3',

            (2,2): '4',
            (2,1): '5',
            (2,0): '6',

            (1,2): '7',
            (1,1): '8',
            (1,0): '9',

            (0,2): 'x',
            (0,1): '0',
            (0,0): 'y',
        }

    def __init__(self, loop):
        super(TouchNumpad, self).__init__(loop)

        # hook needed for IRQ
        global _singleton
        assert not _singleton
        _singleton = self

        self.cols = [Pin(i) for i in ('COL0', 'COL1', 'COL2')]
        self.rows = [Pin(i) for i in ('ROW0', 'ROW1', 'ROW2', 'ROW3')]
        self.pins = self.cols + self.rows

        # Lots of tuning here:
        # - higher CTPH (high pulse length) helps w/ sensitivity and reliability
        # - decrease prescale to speed up acq, but to a point.
        # - CTPH+CTPL has big impact on overal sample time
        # - larger pulse prescale => more noise margin, MAYBE; but too slow to do own averaging
        #
        self.tsc = touch.Touch(channels=self.pins, caps=['CS0', 'CS1', 'CS2'],
                        handler=self.irq, float_unused=0,
                        CTPH=12, CTPL=12, pulse_prescale=4, max_count=16383)

        self.baseline = None
        self.levels = array.array('I', (0 for i in range(NUM_PINS)))
        self.prev_levels = array.array('I', (0 for i in range(NUM_PINS)))
        self.scan_pin = 0

        self.trigger_baseline = False

        # Scan in random order, because tempest.
        # But Tempest? Scan order, when we scan completely, everytime,
        # doesn't reveal anything, and the difference between touch
        # vs no touch is a few millivolts anyway... but harmless?
        self.scan_order = list(range(7))
        shuffle(self.scan_order)

        # begin scanning sequence
        self.loop = loop
        self.start()

    def capture_baseline(self):
        # call this at a time when we feel no keys are pressed (during boot up)
        self.trigger_baseline = True

    @staticmethod
    def irq(tsc):
        # done sampling a Row or Column; store result and continue scan
        self = _singleton
        assert tsc == self.tsc

        val = tsc.finished()
        if val == 0:
            # serious hardware fault? How to report it?
            # also seeing as noise signal when microsd runs
            print("maxcount on %r" % self.scan_pin)
        else:
            self.levels[self.scan_pin] = val

        # must let lines discharge for 1ms
        self.tsc.discharge()

        # do next step, after 1ms delay
        self.loop.call_later_ms(1, self.irq_step2)

    def irq_step2(self):
        # Sample next pin / maybe look at results.
        if self._disabled:
            return
    
        # move to next pin
        self.scan_idx += 1
        if self.scan_idx == NUM_PINS:
            self.scan_idx = 0

            # been around once now; we have some data
            self.calc()

        self.scan_pin = self.scan_order[self.scan_idx]

        # start the next scan
        self.tsc.start_sample(self.pins[self.scan_pin])

    def start(self):
        # Begin scanning for events
        self._disabled = False

        self.scan_idx = 0
        self.scan_pin = self.scan_order[0]

        # prime the irq pump
        self.tsc.start_sample(self.pins[self.scan_pin])

    def calc(self):
        # average history, apply threshold to know which are "down"
        if self.debug == 1:
            print('\x1b[H\x1b[2J\n')
            LABELS = [('col%d' % n) for n in range(3)] + [('row%d' % n) for n in range(4)]
        if self.debug == 2:
            from main import dis
            dis.clear()

        # should we remember this as a reference point (of no keys pressed)
        if self.trigger_baseline:
            self.baseline = array.array('I', self.prev_levels)
            self.trigger_baseline = False

            if 0:
                LABELS = [('col%d' % n) for n in range(3)] + [('row%d' % n) for n in range(4)]
                print("Baselines:")
                for idx in range(NUM_PINS):
                    print('%s: %5d' % (LABELS[idx], self.baseline[idx]))

            return

        pressed = set()
        diffs = array.array('I')

        for idx in range(NUM_PINS):
            # track a running average, using different weights depending on sensitivity mode
            if self.sensitivity == 0:       # "sensitive"
                avg = self.levels[idx]
            elif self.sensitivity == 1:     # "normal"
                avg = (self.prev_levels[idx] + self.levels[idx]) // 2
            elif self.sensitivity == 2:     # "less sensitive"
                avg = ((self.prev_levels[idx]*3) + self.levels[idx]) // 4
            elif self.sensitivity == 3:     # "med. sensitive"
                avg = ((self.prev_levels[idx]*2) + self.levels[idx]) // 3
            else:   #elif self.sensitivity == 4:     # more sensitive
                avg = int((self.prev_levels[idx]*0.25) + (self.levels[idx] * 0.75))

            self.prev_levels[idx] = avg

            if self.baseline:
                diff = self.baseline[idx] - avg
                diffs.append(diff)

                # the critical "threshold" .. remember, values below this are
                # might be "light" touches or proximity. 
                if diff > THRESHOLD:
                    pressed.add(idx)

                # handle baseline drift, in one direction at least
                if diff < 0:
                    self.baseline[idx] = avg

                if self.debug == 1:
                    print('%s: %5d   %4d   %d' % (LABELS[idx], avg, diff, idx in pressed))

                if self.debug == 2:
                    from main import dis
                    y = (idx * 6)+ 3

                    dx = 64 + int(diff/8)
                    dx = min(max(0, dx), 127)
                    dis.dis.pixel(dx, y+1, 1)
                    dis.dis.pixel(dx, y+2, 1)

                    dx = 64 + int(THRESHOLD/8)
                    dis.dis.pixel(dx, y, 1)
                    dis.dis.pixel(dx, y+3, 1)

                    dis.show()

        if max(diffs, default=0) < -10 or (len(pressed) > 4):
            print("auto recal")
            self.baseline = array.array('I', self.prev_levels)

        if self.debug == 1:
            print('\n')
            if diffs:
                print('min_diff = %5d / %5d / %5d' % (
                    min(diffs),
                    (sum(diffs) / len(diffs)),
                    max(diffs)
                ))

        if self.debug == 2: return

        # Consider only single-pressed here; we can detect
        # many 2-key combo's but no plan to support that so they
        # are probably noise from that PoV.
        col_down = [i for i in range(3) if i in pressed]
        row_down = [i-3 for i in range(3, 7) if i in pressed]

        if len(col_down) == 1 and len(row_down) == 1:
            # determine what key
            key = self.DECODER[(row_down[0], col_down[0])]
        else:
            # not sure, or all up
            key = ''

        self._key_event(key)
    
# EOF
