from tidal import *
from buttons import Buttons
from app import TextApp
import machine
from machine import ADC
import time
import lodepng
from scheduler import get_scheduler

import vga2_8x8 as font

PATH = "/apps/tidal_oscilloscope/"

GREY = color565(0x20, 0x20, 0x20)

SAMPLES = 128       # width of scope area = number of samples
SCOPE_HEIGHT = 128

SCALES = [[ADC.ATTN_0DB, 100], [ADC.ATTN_6DB, 200], [ADC.ATTN_11DB, 400]]
TIMEBASES = [[31, 1], [62, 2], [156, 5], [312, 10], [625, 20], [1562, 50], [3125, 100], [6250, 200], [0, 1000], [0, 2000], [0, 5000], [0, 10000], [0, 20000], [0, 60000]]
ACQUIRE_MODE_ASYNC_THRESH = 101
PX_PER_VDIV = 16
PX_PER_HDIV = 32

TRIGGER_WIDTH = 1
MAX_TRIGGER_CYCLES = 256

with open(PATH + "version") as f:
    VERSION = f.read()

class Oscilloscope(TextApp):
    BG = BLACK
    FG = WHITE

    def on_start(self):
        self.set_rotation(270)
        super().on_start()

    def on_activate(self):
        super().on_activate()

        (w, h, buf) = lodepng.decode565(PATH + "badgilent.png")
        display.blit_buffer(buf, 0, 0, w, h)
        display.text(font, "V{}".format(VERSION), 112, 120, BLUE, WHITE)
        time.sleep(2.5)
        display.fill(BLACK)
        display.fill_rect(SAMPLES, 0, 240-SAMPLES, SCOPE_HEIGHT+1, GREY)

        # The badge doesn't sleep when connected to USB.
        # Roll mode is broken when not connected to USB, unless sleeping is disabled - why?
        get_scheduler().set_sleep_enabled(False)

        self.should_quit = False
        self.scale = 0
        self.timebase = 4
        self.trig = False
        self.trig_voltage = 0.4
        self.buffer0 = bytearray(SAMPLES)
        self.roll_mode = False
        self.roll_display_cnt = 0
        self.adc_init()
        self.source = self.get_adc
        self.draw_info()

        self.buttons.set_rotation(0) # Something seems to be broken with the buttons when the screen is rotated 90/270 degrees...
        # self.buttons.on_press(BUTTON_A, lambda: self.buttona())
        self.buttons.on_press(JOY_LEFT, lambda: self.btn_ud(-1))
        self.buttons.on_press(JOY_RIGHT, lambda: self.btn_ud(1))
        self.buttons.on_press(JOY_UP, lambda: self.timebase_set(-1))
        self.buttons.on_press(JOY_DOWN, lambda: self.timebase_set(1))
        
        self.start_new_acquisition()

    def start_new_acquisition(self):
        if not self.roll_mode and not self.should_quit:
            self.acq_timer = self.after(0, self.acquisition_start)

    def on_deactivate(self):
        self.should_quit = True
        self.acq_timer.cancel()

    def clear_buffer(self):
        self.buffer0 = bytearray(SAMPLES)

    def px_to_volts(self, val):
        return val / PX_PER_VDIV * (SCALES[self.scale][1] / 1000)

    def btn_ud(self, dx):
        if BUTTON_A.value() == 1:
            self.scale_set(dx)
        else:
            # A held
            self.trigger_set(dx)

    def scale_set(self, ds):
        if 0 <= (self.scale + ds) < len(SCALES):
            self.scale += ds
            self.adc_init()
            if self.roll_mode:
                self.clear_buffer()
            self.draw_info()

    def timebase_set(self, dt):
        if 0 <= (self.timebase + dt) < len(TIMEBASES):
            self.timebase += dt
            self.adc_init()

            if TIMEBASES[self.timebase][0] == 0:
                self.roll_mode_init(True, TIMEBASES[self.timebase][1])
            else:
                self.roll_mode_init(False)
            self.draw_info()

    def trigger_set(self, dt):
        self.trig_voltage += dt * 0.5 * SCALES[self.scale][1] / 1000
        self.draw_info()

    def adc_init(self):
        self.adc0 = ADC(G0, atten=SCALES[self.scale][0])
        self.vscaling = int((SCALES[self.scale][1] * 1000) / PX_PER_VDIV)

    def roll_mode_init(self, roll, ms_per_div=0):
        if roll:
            self.roll_mode = True
            self.acq_timer.cancel()
            self.clear_buffer()
            self.acq_timer = self.periodic(ms_per_div/PX_PER_HDIV, self.acquire_rollmode)
            self.trig = False
        else:
            if not self.roll_mode: return
            self.roll_mode = False
            self.acq_timer.cancel()
            self.start_new_acquisition()



    # Acquisition

    def get_adc(self):
        return self.adc0.read_uv() // self.vscaling

    def acquire_buffer(self, buffer, n, timebase_setting):
        ts_microseconds = TIMEBASES[timebase_setting][0]

        # 1 ms/div requires supa hax
        if timebase_setting == 0:
            x = 0
            for i in range(n):
                buffer[i] = self.source()
                x = i * 0.33 # nop()
            return

        for i in range(n):
            t0 = time.ticks_us()
            buffer[i] = self.source()
            # Improve timebase accuracy by factoring in the acquisition time
            # 50 Hz noise on 20ms/div setting should line up with the grid lines
            t_adc = time.ticks_diff(time.ticks_us(), t0)
            time.sleep_us(ts_microseconds - t_adc)

    def acquire_rollmode(self):
        for i in range(SAMPLES-1):
            self.buffer0[i] = self.buffer0[i+1]
        self.buffer0[SAMPLES-1] = self.source()

        self.roll_display_cnt -= 1
        if self.roll_display_cnt < 0:
            self.after(0, self.draw_samples)
            # For faster rolling timebases, don't draw every new sample
            if TIMEBASES[self.timebase][1] == 2000:
                self.roll_display_cnt = 1
            elif TIMEBASES[self.timebase][1] == 1000:
                self.roll_display_cnt = 3
            else:
                self.roll_display_cnt = 0

    def acquire_async(self):
        if self.roll_mode:
            self.acq_timer.cancel()
            return
        self.buffer0[self.bufidx] = self.adc0.read_uv() // self.vscaling
        self.bufidx += 1
        if self.bufidx == SAMPLES:
            self.acq_timer.cancel()
            self.after(0, self.draw_samples)

    def acquisition_start(self):
        # Try to trigger, give up after MAX_TRIGGER_CYCLES
        trig_attempts = 0
        trig_state = 0
        triggered = 0
        trigbuf = bytearray(TRIGGER_WIDTH)
        trig_level = int(self.trig_voltage * 1000000) // self.vscaling
        while trig_attempts < MAX_TRIGGER_CYCLES:
            self.acquire_buffer(trigbuf, TRIGGER_WIDTH, 0)
            if trig_state == 0:
                triggered = 1
                for val in trigbuf:
                    triggered = triggered & (val < trig_level)
                if triggered: trig_state = 1
            elif trig_state == 1:
                triggered = 1
                for val in trigbuf:
                    triggered = triggered & (val >= trig_level)
                self.trig = triggered
                if triggered: break

            trig_attempts += 1

        # If the timebase is too long, get the samples in a timer callback so the UI stays responsive to button presses
        if TIMEBASES[self.timebase][1] >= ACQUIRE_MODE_ASYNC_THRESH:
            self.bufidx = 0
            self.acq_timer = self.periodic(TIMEBASES[self.timebase][0] / 1000, self.acquire_async)
        else:
            self.acquire_buffer(self.buffer0, SAMPLES, self.timebase)
            self.after(0, self.draw_samples)


    # Drawing

    def draw_info(self):
        display.text(font, "{: 4} mV/div".format(SCALES[self.scale][1]), 148, 16, WHITE, GREY)
        if TIMEBASES[self.timebase][1] < 1000:
            display.text(font, "{: 4} ms/div".format(TIMEBASES[self.timebase][1]), 148, 28, WHITE, GREY)
        else:
            display.text(font, "{: 4.0f} s/div ".format(TIMEBASES[self.timebase][1] / 1000), 148, 28, RED, GREY)

        if self.roll_mode:
            trig_msg = "            "
        else:
            trig_msg = "Trig: {:.2f} V".format(self.trig_voltage)
        display.text(font, trig_msg, 140, 40, WHITE, GREY)

        display.text(font, "Src: Pin G0", 148, 52, YELLOW, GREY)
        

    def draw_samples(self):
        if self.should_quit: return

        # To reduce flicker, clear and redraw one horizontal division at a time
        trig_level = int(self.trig_voltage * 1000000) // self.vscaling
        HDIVS = 4
        for div in range(HDIVS):
            x0 = div * PX_PER_HDIV
            display.fill_rect(x0, 0, PX_PER_HDIV, SCOPE_HEIGHT, BLACK)
            
            # Vertical gridlines
            display.line(x0+PX_PER_HDIV-1, 0, x0+PX_PER_HDIV-1, SCOPE_HEIGHT, GREY)
            
            # Horizontal gridlines
            for j in range(8):
                display.line(x0, SCOPE_HEIGHT-j*PX_PER_VDIV, x0+PX_PER_HDIV, SCOPE_HEIGHT-j*PX_PER_VDIV, GREY)
            
            # Trigger level
            if not self.roll_mode:
                display.line(x0, SCOPE_HEIGHT-1-trig_level, x0+PX_PER_HDIV-1, SCOPE_HEIGHT-1-trig_level, GREEN)
            
            # Trace
            pidx = 0
            if x0 > 0:
                pidx = x0 - 1
            prev_y = SCOPE_HEIGHT-1 - self.buffer0[pidx]
            prev_x = x0
            for x1 in range(PX_PER_HDIV):
                x = x0 + x1
                y = SCOPE_HEIGHT-1 - self.buffer0[x]
                display.line(prev_x, prev_y, x, y, YELLOW)
                prev_x = x
                prev_y = y

        # Sidebar
        display.text(font, "Trig'd" if self.trig else "      ", 132, 4, GREEN, GREY)

        # Measurements
        val_max = max(self.buffer0)
        val_min = min(self.buffer0)
        val_avg = sum(self.buffer0) / len(self.buffer0)
        display.text(font, "Min: {:.2f} V".format(self.px_to_volts(val_min)), 140, 76, YELLOW, GREY)
        display.text(font, "Max: {:.2f} V".format(self.px_to_volts(val_max)), 140, 88, YELLOW, GREY)
        display.text(font, "Avg: {:.2f} V".format(self.px_to_volts(val_avg)), 140, 100, YELLOW, GREY)

        self.start_new_acquisition()

main = Oscilloscope