SOFTWARE_VERSION_NUMBER = "1.0.0"
DEVICE_TYPE = "Helio-STELLA"
# Helio-STELLA spectrometer instrument
# NASA open source software license
# Paul Mirel 2024
import time
start_time = time.monotonic()

# gain settings: 0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512
spectral_gain = 2 # calibrated at gain of 2. Gain of 4 saturates in the red channel in direct sunlight.
print( "SPECTRAL GAIN = {}".format( spectral_gain ))

# starts up in continuous recording and remains in continuous recording.
# click the button to cycle displays (response is a bit slow ~1s)
# press-and-hold (3 sec) to turn on the reference lamp,
# press-and-hold again to turn off the reference lamp.

import gc
gc.collect()
start_mem_free_kB = gc.mem_free()/1000
print("start memory free {0:.2f} kB".format( start_mem_free_kB ))

import os
import microcontroller
import board
import digitalio
import rtc
import neopixel
import storage
import sdcardio
import busio
import adafruit_max1704x
import adafruit_pcf8523
import displayio
import terminalio
import adafruit_displayio_ssd1306
from adafruit_display_text import label
import vectorio # for shapes
from i2c_button import I2C_Button
from adafruit_as7341 import AS7341
import adafruit_ltr390
#optional sensors
from adafruit_bme280 import basic as adafruit_bme280

def main():
    #sphere_calibration_counts_per_radiance = 7439, 46384, 32633, 29581, 25829, 30338, 33678, 40528 #preliminary
    tsis_cal_counts_per_irradiance = 1405.9, 2079.6, 2631.6, 3556.8, 4246.0, 5060.6, 6888.9, 9130.9
    # data from as7341 datasheet and ltr390 datasheet
    color_designations = "violet", "indigo", "blue", "cyan", "green", "yellow", "orange", "red"
    lux_center_nm = 550
    uva_center_nm = 320
    vis_band_center_uncertainty_nm = 10
    vis_irradiance_uncertainty_percent = 60

    # from as7341 datasheet
    count_max = 65535
    vis_band_designations = 415, 445, 480, 515, 555, 590, 630, 680
    bandwidths_fwhm_uv = 100, 70
    bandwidths_fwhm_vis =  26,  30,  36,  39,  39,  40,  50,  52
    response_at_ee_counts =  55, 110, 210, 390, 590, 840, 1350, 1070
    ee_calibration_uW_per_cm_squared = 107.67
    gain_at_cal = 64
    spectral_field_of_view_fwhm_deg = 80

    # from ltr390 uv and lux sensor datasheet
    uva_field_of_view_fwhm_deg = 94
    lux_uncertainty = 10
    lux_saturated = 52427

    DAYS = { 0:"Sunday", 1:"Monday", 2:"Tuesday", 3:"Wednesday", 4:"Thursday", 5:"Friday", 6:"Saturday" }
    DATA_FILE = "/sd/data.csv"
    LOW_BATTERY_PERCENT = 20
    RED =   ( 0, 255, 0 )
    ORANGE =( 28, 64, 0 )
    YELLOW =( 64, 64, 0 )
    GREEN = ( 255, 0, 0 )
    BLUE =  ( 0, 0, 255 )
    OFF =   ( 0, 0, 0 )
    band_center_error_plus_minus = 10
    spectral_units = "counts" #"uW/cm^2"

    memory_check( "begin main()", start_mem_free_kB )
    displayio.release_displays()
    UID = int.from_bytes(microcontroller.cpu.uid, "big") % 10000
    print("unique identifier (UID) : {0}".format( UID ))
    onboard_blue_led = initialize_led( board.LED )
    number_of_onboard_neopixels = 1
    onboard_neopixel = initialize_neopixel( board.NEOPIXEL, number_of_onboard_neopixels )
    vfs, sdcard = initialize_sd_card_storage( onboard_neopixel, RED )
    i2c_bus = initialize_i2c_bus( board.SCL, board.SDA, onboard_neopixel, GREEN, YELLOW, OFF )
    try:
        WX_sensor = adafruit_bme280.Adafruit_BME280_I2C( i2c_bus )
        print( "found WX_sensor" )
    except ValueError:
        WX_sensor = False
    gc.collect()
    header = ( "device_type, software_version, UID, batch, weekday, "
        "timestamp_iso8601, decimal_hour, lux_center_nm, lux, uva_center_nm, uva, "
        "visible_band_center_uncertainty, visible_band_irradiance_uncertainty_%, "
        "violet_center_nm, violet_counts, violet_irradiance_W/(m^2*nm), indigo_center_nm, indigo_counts, indigo_irradiance_W/(m^2*nm), "
        "blue_center_nm, blue_counts, blue_irradiance_W/(m^2*nm), cyan_center_nm, cyan_counts, cyan_irradiance_W/(m^2*nm), "
        "green_center_nm, green_counts, green_irradiance_W/(m^2*nm), yellow_center_nm, yellow_counts, yellow__irradiance_W/(m^2*nm), "
        "orange_center_nm, orange_counts, orange_irradiance_W/(m^2*nm), red_center_nm, red_counts, red_irradiance_W/(m^2*nm), "
        "battery_voltage, battery_percent" )
    if WX_sensor:
        header = header + (", air_temperature_C, air_temperature_uncertainty_C, relative_humidity_percent, relative_humidity_uncertainty_percent, "
        "barometric_pressure_hPa, barometric_pressure_uncertainty_hPa, altitude_uncalibrated_m, altitude_uncalibrated_uncertainty_m\n")
    else:
        header = header + ("\n")
    print( header )

    WX_sensor_air_temperature_uncertainty = 1
    WX_sensor_relative_humidity_uncertainty = 3
    WX_sensor_barometric_pressure_uncertainty = 1
    WX_sensor_altitude_uncertainty = 500
    gc.collect()
    data_file_exists = False
    if vfs:
        storage.mount(vfs, "/sd")
        data_file_exists = initialize_data_file( header, DATA_FILE )
        recording = True
    else:
        recording = False
    del header
    gc.collect()

    hardware_clock, hardware_clock_battery_OK = initialize_real_time_clock( i2c_bus )
    batch_number = 0
    if hardware_clock:
        system_clock = rtc.RTC()
        system_clock.datetime = hardware_clock.datetime
        batch_number = update_batch( hardware_clock.datetime )
    print( "batch number == {}".format( batch_number ))
    spectral_sensor = initialize_spectral_sensor( i2c_bus )
    # gain settings: 0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512
    # gain 8 saturates in direct sunlight
    if spectral_sensor:
        spectral_sensor.gain = spectral_gain
    uva_sensor = initialize_uva_sensor ( i2c_bus )
    button = initialize_button( i2c_bus )
    battery_monitor = initialize_battery_monitor( i2c_bus )
    battery_voltage, battery_percent = check_battery( battery_monitor, onboard_blue_led )

    display, display_group = initialize_display( i2c_bus )
    number_of_display_states = 11
    if WX_sensor:
        number_of_display_states = 12
    display_state = 0

    if display:
        if hardware_clock:
            timenow = hardware_clock.datetime
        else:
            timenow = time.struct_time(( 2000,  01,   01,   00,  00,  00,   0,   -1,    -1 ))
        iso8601_utc = "{:04}{:02}{:02}T{:02}{:02}{:02}Z".format(
            timenow.tm_year, timenow.tm_mon, timenow.tm_mday,
            timenow.tm_hour, timenow.tm_min, timenow.tm_sec )
        weekday = DAYS[ timenow.tm_wday ]
        display.show( display_group )

        date_group = displayio.Group(scale=2, x=3, y=8)
        text = "{}-{:02}-{:02}".format(timenow.tm_year, timenow.tm_mon, timenow.tm_mday)
        text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        date_group.append(text_area) # Subgroup for text scaling

        display_group.append(date_group)

        time_group = displayio.Group(scale=1, x=3, y=28)
        text = "{:02}:{:02} UTC".format( timenow.tm_hour, timenow.tm_min )
        batch_number_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        time_group.append(batch_number_area) # Subgroup for text scaling
        display_group.append(time_group)
        print( "show date and time on display" )
        time.sleep(3)
        display_group.pop()
        display_group.pop()

        text_group = displayio.Group(scale=2, x=0, y=15)
        text = "U:" + str(UID)
        text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        text_group.append(text_area) # Subgroup for text scaling

        display_group.append(text_group)

        batch_number_group = displayio.Group(scale=2, x=80, y=15)
        if vfs:
            text = "B:"+str( batch_number )
        else:
            text = "NoSD"
        batch_number_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        batch_number_group.append(batch_number_area) # Subgroup for text scaling
        display_group.append(batch_number_group)
        print( "show UID and batch on display" )
        time.sleep(3)
        display_group.pop()
        display_group.pop()

        text_group = displayio.Group(scale=2, x=0, y=15)
        text = "battery".format(battery_voltage)
        text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        text_group.append(text_area) # Subgroup for text scaling
        display_group.append(text_group)

        battery_percent_group = displayio.Group(scale=2, x=90, y=15)
        text = "{}%".format( int( battery_percent ))
        battery_percent_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        battery_percent_group.append(battery_percent_area) # Subgroup for text scaling
        display_group.append(battery_percent_group)
        print( "show battery voltage and percent on display" )

        time.sleep(2)
        display_group.pop()
        display_group.pop()

        #show date and time on display here TBD

        graph_bar, graph_bar_x, batch_number_label_text_area, polygon = create_graph_screen( display_group, vis_band_designations )

        waveband_display_group = displayio.Group()
        wavelength_group = displayio.Group(scale=2, x=4, y=8)
        text = "WL"
        wavelength_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        wavelength_group.append(wavelength_text_area) # Subgroup for text scaling
        waveband_display_group.append(wavelength_group)

        colorname_group = displayio.Group(scale=1, x=4, y=26)
        text = "colorname"
        colorname_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        colorname_group.append(colorname_text_area) # Subgroup for text scaling
        waveband_display_group.append(colorname_group)

        intensity_group = displayio.Group(scale=2, x=50, y=15)
        text = "value"
        intensity_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        intensity_group.append( intensity_text_area )
        waveband_display_group.append( intensity_group )

        WX_display_group = displayio.Group()
        T_label_group = displayio.Group(scale=2, x=50, y=8)
        text = "C"
        T_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        T_label_group.append(T_label_text_area) # Subgroup for text scaling
        WX_display_group.append(T_label_group)

        T_group = displayio.Group(scale=2, x=0, y=8)
        text = " 0.0"
        T_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        T_group.append(T_text_area) # Subgroup for text scaling
        WX_display_group.append(T_group)

        RH_label_group = displayio.Group(scale=2, x=111, y=8)
        text = "%"
        RH_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        RH_label_group.append(RH_label_text_area) # Subgroup for text scaling
        WX_display_group.append(RH_label_group)

        RH_value_group = displayio.Group(scale=2, x=85, y=8)
        text = ""
        RH_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        RH_value_group.append(RH_value_text_area) # Subgroup for text scaling
        WX_display_group.append(RH_value_group)

        BP_label_group = displayio.Group(scale=1, x=0, y=28)
        text = "barometer        hPa"
        BP_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        BP_label_group.append(BP_label_text_area) # Subgroup for text scaling
        WX_display_group.append(BP_label_group)

        BP_value_group = displayio.Group(scale=1, x=62, y=28)
        text = "1000.0"
        BP_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        BP_value_group.append(BP_value_text_area) # Subgroup for text scaling
        WX_display_group.append(BP_value_group)




    end_time = time.monotonic()
    print("instrument startup time == {} s".format(int(end_time - start_time)))
    gc.collect()
    interval_start_time_s = time.monotonic()
    loop_count = 0
    lux = 0
    operational = True
    lamp_on = False
    last_button_values = [False, False, False]
    click_allow = True
    click_clear_cycles = 1
    click_loop_count = 0

    sample_interval_s = 1.0
    try:
        while operational:
            loop_begin_time_s = time.monotonic()
            loop_count += 1
            if loop_count > 65535:
                loop_count = 0
            gc.collect()
            if display:
                if display_state == 0:
                    display.show( display_group )
                    display_group.hidden = False
                else:
                    display_group.hidden = True

                if WX_sensor:
                    WL_display_range = (3, 12)
                    WL_chooser = display_state - 1
                    if display_state == 1:
                        display.show( WX_display_group )
                else:
                    WL_display_range = (2, 11)
                    WL_chooser = display_state
                #print("WL_chooser = {}".format(WL_chooser))

                if WL_chooser == 1:
                    display.show( waveband_display_group )
                    wavelength_text_area.text = str(lux_center_nm)
                    colorname_text_area.text = "lux"
                    if lux < lux_saturated:
                        intensity_text_area.text = str( int( lux ))
                    else:
                        intensity_text_area.text = "sat"

                if WL_chooser == 2 :
                    display.show( waveband_display_group )
                    wavelength_text_area.text = str(uva_center_nm)
                    colorname_text_area.text = "uva"
                    intensity_text_area.text = str( uva_counts )


                if WL_chooser in range (3,11) :
                    display.show( waveband_display_group )
                    wavelength_text_area.text = str(vis_band_designations[WL_chooser-3])
                    colorname_text_area.text = str(color_designations[WL_chooser-3])
                    intensity_text_area.text = str( round(normalized_vis_data[WL_chooser-3],3) )


            if button:
                if not click_allow:
                    if loop_count > click_loop_count + click_clear_cycles:
                        click_allow = True
                        button.clear()
                        print(" click allow = True")
                button_values = button.status
                if button_values[ 1 ]:
                    print( "button clicked" )
                    if click_allow:
                        display_state = (display_state + 1) % number_of_display_states
                        print( "display state = {}".format( display_state ))
                    #batch_number = update_batch( hardware_clock.datetime )
                    button.led_bright = 16
                    button.clear()
                else:
                    button.led_bright = 1
                if button_values[ 2 ]:
                    print("button pressed")
                    time.sleep(0.1)
                    hold_time = 0
                    start_hold_time = time.monotonic()
                    while button_values[ 2 ] and hold_time <= 2.2:
                        button_values = button.status
                        hold_time = time.monotonic() - start_hold_time
                        time.sleep(0.1)
                    button.clear()
                    if hold_time > 2:
                        click_allow = False
                        lamp_on = not lamp_on
                        if lamp_on:
                            print( "turn on the reference lamp")
                            spectral_sensor.led = True
                        else:
                            print( "turn off the reference lamp")
                            spectral_sensor.led = False
                        click_allow = False
                        click_loop_count = loop_count
                    else:
                        #this is the same as a click
                        print( "button press-release clicked" )
                        if click_allow:
                            display_state = (display_state + 1) % number_of_display_states
                            print( "display state = {}".format( display_state ))
                        #batch_number = update_batch( hardware_clock.datetime )
                        button.led_bright = 16
                        button.clear()

            # check if it is time to read a new datapoint. If not yet, then skip this section
            if time.monotonic() > interval_start_time_s + sample_interval_s:
                gc.collect()
                if display:
                    if recording:
                        if vfs:
                            batch_number_label_text_area.text = "batch:" + str( batch_number )
                        else:
                            batch_number_label_text_area.text = "NoSDcard"
                    else:
                        if vfs:
                            batch_number_label_text_area.text = "lastB:" + str( batch_number )
                        else:
                            batch_number_label_text_area.text = "NoSDcard"

                if hardware_clock:
                    timenow = hardware_clock.datetime
                else:
                    timenow = time.struct_time(( 2020,  01,   01,   00,  00,  00,   0,   -1,    -1 ))
                interval_start_time_s = time.monotonic()
                mem_free_kB = gc.mem_free()/1000
                #print( "\nmemory free == {} kB, {:.1f} %".format( mem_free_kB, 100 * mem_free_kB/start_mem_free_kB ))
                check_loop_count = loop_count
                if WX_sensor:
                    WX_reading = (WX_sensor.temperature, WX_sensor.relative_humidity, WX_sensor.pressure, WX_sensor.altitude)
                    if WX_reading[0] < 10:
                        T_text_area.text = " " + str(round(WX_reading[0],1))
                    else:
                        T_text_area.text = str(round(WX_reading[0],1))
                    RH_value_text_area.text = str( int( WX_reading[1]))
                    if WX_reading[2] < 1000:
                        BP_value_text_area.text = " " + str( round( WX_reading[2],1 ))
                    else:
                        BP_value_text_area.text = str( round( WX_reading[2],1 ))
                else:
                    WX_reading = ( 0, 0, 0, 0 )
                if uva_sensor:
                    uva_counts = uva_sensor.uvs
                    #print( uva_counts )
                    lux = uva_sensor.lux
                    #print( lux )
                else:
                    uva_counts = 0
                    lux = 0
                spectral_data_counts = []
                if spectral_sensor:
                    vis_data_counts = spectral_sensor.all_channels
                else:
                    vis_data_counts = 0,0,0,0,0,0,0,0
                normalized_vis_data = []
                for n in range (0, len( vis_data_counts )):
                    #
                    #
                    # do normalization calculations for visible light bands here
                    # convert counts to irradiance (W/(m^2*nm))
                    #
                    #normalized_vis_data.append( vis_data[n]/ sphere_calibration_counts_per_radiance[n]/ bandwidths_fwhm_vis[n] )
                    normalized_vis_data.append( vis_data_counts[n]/ tsis_cal_counts_per_irradiance[n])
                    #
                    #
                    #
                #print(normalized_vis_data)
                #print( vis_data_counts )
                if display:
                    scaled_graph_data = []
                    for item in normalized_vis_data:
                        scaled_graph_data.append(item *1000)
                    graph_data( scaled_graph_data, graph_bar, graph_bar_x, polygon ) # 8 ms


                battery_voltage, battery_percent = check_battery( battery_monitor, onboard_blue_led )
                gc.collect()
                iso8601_utc = "{:04}{:02}{:02}T{:02}{:02}{:02}Z".format(
                    timenow.tm_year, timenow.tm_mon, timenow.tm_mday,
                    timenow.tm_hour, timenow.tm_min, timenow.tm_sec )
                decimal_hour = timestamp_to_decimal_hour( timenow )
                weekday = DAYS[ timenow.tm_wday ]
                if lux < lux_saturated:
                    pass
                else:
                    lux = 99999
                datapoint_string = ""
                datapoint_string += "{}, {}, ".format( DEVICE_TYPE, SOFTWARE_VERSION_NUMBER)
                datapoint_string += ( "{}, {}, {}, {}, {}, ".format( UID, batch_number, weekday, iso8601_utc, decimal_hour ))
                datapoint_string += ( "{}, {}, {}, {}, ".format( lux_center_nm, lux, uva_center_nm, uva_counts ))
                datapoint_string += ( "{}, {}, ".format( vis_band_center_uncertainty_nm, vis_irradiance_uncertainty_percent ))
                for i in range (len(vis_band_designations)):
                    datapoint_string += str( vis_band_designations[i] )
                    datapoint_string += ", "
                    datapoint_string += str( vis_data_counts[i] )
                    datapoint_string += ", "
                    datapoint_string += str( normalized_vis_data[i] )
                    datapoint_string += ", "

                datapoint_string += ( "{:.2f}, {}".format( battery_voltage, int(battery_percent )))
                if WX_sensor:
                    datapoint_string += str ( ", " )
                    datapoint_string += str ( int( WX_reading[0] ))
                    datapoint_string += str ( ", " )
                    datapoint_string += str ( WX_sensor_air_temperature_uncertainty )
                    datapoint_string += str ( ", " )
                    datapoint_string += str ( int( WX_reading[1] ))
                    datapoint_string += str ( ", " )
                    datapoint_string += str ( WX_sensor_relative_humidity_uncertainty )
                    datapoint_string += str ( ", " )
                    datapoint_string += str ( int( WX_reading[2] ))
                    datapoint_string += str ( ", " )
                    datapoint_string += str ( WX_sensor_barometric_pressure_uncertainty)
                    datapoint_string += str ( ", " )
                    datapoint_string += str ( WX_reading[3] )
                    datapoint_string += str ( ", " )
                    datapoint_string += str ( WX_sensor_altitude_uncertainty )

                # write data to file:
                write_data_to_file( DATA_FILE, datapoint_string, button, onboard_neopixel, OFF, GREEN, ORANGE )
                # mirror data to usb:
                print ( datapoint_string )
            time.sleep(0.1)
            loop_time = (time.monotonic() - loop_begin_time_s)

    finally:  # clean up the busses when ctrl-c'ing out of the loop
        if sdcard:
            sdcard.deinit()
            print( "sd card deinitialized" )
        displayio.release_displays()
        print( "displayio displays released" )
        i2c_bus.deinit()
        print( "i2c_bus deinitialized" )

def graph_data( spectral_data_sorted, graph_bar, graph_bar_x, polygon ):
    if spectral_data_sorted:
        y_zero_pixels = 31
        y_upper_pixels = 17
        graph_points = [(2, 112), (2, 104)]
        y_span_pixels = y_zero_pixels - y_upper_pixels
        irradiances = spectral_data_sorted
        irrad_min = min( irradiances )
        irrad_max = max( irradiances )
        irrad_span = irrad_max - irrad_min
        if irrad_span < 1:
            irrad_span = 1
        for count in range (len( irradiances )):
            irrad_value = irradiances[ count ]
            irrad_height = irrad_value - irrad_min
            irrad_normalized_height = irrad_height/ irrad_span
            irrad_bar_height_pixels = int(y_span_pixels * irrad_normalized_height)
            irrad_drop_height_pixels = int(y_span_pixels - irrad_bar_height_pixels)
            irrad_y_top_pixel = int(y_upper_pixels + irrad_drop_height_pixels)
            graph_bar[count].y = irrad_y_top_pixel
            graph_bar[count].height = 1
            point = (graph_bar_x[count]+1, irrad_y_top_pixel)
            graph_points.append(point)
        graph_points.append((126, 104))
        graph_points.append((126, 112))
        polygon.points = graph_points
        #del y_zero_pixels, y_upper_pixels, graph_points, y_span_pixels, irradiances, spectral_data_sorted
        #del irrad_min, irrad_max, irrad_span, count, irrad_value, irrad_height
        #del irrad_normalized_height, irrad_bar_height_pixels, irrad_drop_height_pixels
        #del irrad_y_top_pixel, point

def create_graph_screen( display_group, vis_band_designations ):
    if display_group is not False:
        palette = displayio.Palette(1)
        palette[0] = 0xFFFFFF
        points2 = [ (2, 32), (2, 30), (126, 30), (126, 32)]
        polygon = vectorio.Polygon(pixel_shader=palette, points=points2, x=0, y=0)
        display_group.append( polygon )
        bar_color = 0xFFFFFF
        bar = displayio.Palette(1)
        bar[0] = bar_color
        bar_width = 1
        bar_default_height = 1
        #graph_bar_x = ( -1, 1, 33, 43, 55, 67, 81, 92, 106, 123 )
        graph_bar_x = (0, 14, 31, 48, 68, 85, 104, 128)
        graph_bar=[]
        for count in range( len( vis_band_designations) ):
            graph_bar.append( vectorio.Rectangle(pixel_shader=bar, width=bar_width, height=bar_default_height, x=graph_bar_x[count], y=106))
            display_group.append( graph_bar[count] )

        wavelength_label_group = displayio.Group( scale=1, x=4, y=9)
        wavelength_label_text = "415nm          680nm"
        wavelength_label_text_area = label.Label( terminalio.FONT, text=wavelength_label_text, color=0xFFFFFF )
        wavelength_label_group.append( wavelength_label_text_area )
        display_group.append( wavelength_label_group )

        batch_number_label_group = displayio.Group( scale=1, x=38, y=9)
        batch_number_label_text = "batch:"
        batch_number_label_text_area = label.Label( terminalio.FONT, text=batch_number_label_text, color=0xFFFFFF )
        batch_number_label_group.append( batch_number_label_text_area )
        display_group.append( batch_number_label_group )

        print( "initialized graph screen" )
        return ( graph_bar, graph_bar_x, batch_number_label_text_area, polygon )
    else:
        print( "graph screen initialization failed" )
        return False, False, False, False

def read_spectral_sensor( spectral_sensor, band_designations ):
    pass
    return False

def initialize_button( i2c_bus ):
    try:
        button = I2C_Button( i2c_bus )
        print( "initialized_button" )
        blink( button, 1, 0.1, 1, 64 ) #object, count, interval, low_level, high_level
    except ValueError:
        button = False
        print( "button device not found" )
    return button

def initialize_display( i2c_bus ):
    try:
        display_bus = displayio.I2CDisplay( i2c_bus, device_address=0x3c )
        display = adafruit_displayio_ssd1306.SSD1306( display_bus, width=128, height=32 )
        display_group = displayio.Group()
        print( "initialized display" )
    except ValueError:
        display = False
        display_group = False
    return display, display_group

def initialize_battery_monitor( i2c_bus ):
    try:
        monitor = adafruit_max1704x.MAX17048(i2c_bus)
    except:
        print( "battery monitor failed to initialize" )
        monitor = False
    return monitor

def check_battery( monitor, LED):
    if monitor:
        battery_voltage = monitor.cell_voltage
        battery_percent = monitor.cell_percent
        time.sleep(0.2)
        battery_voltage = monitor.cell_voltage
        battery_percent = monitor.cell_percent
        if battery_percent < 25:
                count = 4
                interval = 0.1
                for n in range( 0, count ):
                    LED.value = True
                    time.sleep(interval)
                    LED.value = False
                    time.sleep(interval)
    else:
        battery_voltage = 0
        battery_percent = 0
    return battery_voltage, battery_percent

def initialize_spectral_sensor( i2c_bus ):
    try:
        pass
    except ValueError as err:
        print( "spectral sensor initialization failed: {}".format(err))
        spectral_sensor = False
    return spectral_sensor

def initialize_uva_sensor( i2c_bus ):
    try:
        pass
    except ValueError as err:
        print( "spectral sensor initialization failed: {}".format(err))
        uva_sensor = False
    return uva_sensor

def timestamp_to_decimal_hour( timestamp ):
    try:
        decimal_hour = timestamp.tm_hour + timestamp.tm_min/60.0 + timestamp.tm_sec/3600.0
        return decimal_hour
    except ValueError as err:
        print( "Error: invalid timestamp: {:}".format(err) )
        return False

def update_batch( timestamp ):
    gc.collect()
    datestamp = "{:04}{:02}{:02}".format( timestamp.tm_year, timestamp.tm_mon, timestamp.tm_mday)
    try:
        with open( "/sd/batch.txt", "r" ) as b:
            try:
                previous_batchfile_string = b.readline()
                previous_datestamp = previous_batchfile_string[ 0:8 ]
                previous_batch_number = int( previous_batchfile_string[ 8: ])
            except ValueError:
                previous_batch_number = 0
            if datestamp == previous_datestamp:
                # this is the same day, so increment the batch number
                batch_number = previous_batch_number + 1
            else:
                # this is a different day, so start the batch number at 0
                batch_number = 0
    except OSError:
            print( "batch.txt file not found" )
            batch_number = 0

    batch_string = ( "{:03}".format( batch_number ))
    batch_file_string = datestamp + batch_string
    try:
        with open( "/sd/batch.txt", "w" ) as b:
            b.write( batch_file_string )
    except OSError as err:
        print("Error: writing batch.txt {:}".format(err) )
        pass
    batch_string = ( "{:}".format( batch_number ))
    return batch_string

def initialize_real_time_clock( i2c_bus ):
    null_time = time.struct_time(( 2020,  01,   01,   00,  00,  00,   0,   -1,    -1 ))
    try:
        real_time_clock = adafruit_pcf8523.PCF8523( i2c_bus )
        clock_battery_low = real_time_clock.battery_low
        if clock_battery_low:
            print( "clock battery is low. replace clock battery" )
        else:
            print( "clock battery is OK" )
        timenow = real_time_clock.datetime
        if timenow.tm_wday not in range ( 0, 7 ):
            real_time_clock.datetime = null_time
        timenow = real_time_clock.datetime
    except (ValueError, NameError) as err:
        print( "hardware clock fail: {}".format(err))
        real_time_clock = False
        clock_battery_low = True
        timenow = null_time
    if real_time_clock:
        # set the microcontroller system clock to real time
        system_clock = rtc.RTC()
        system_clock.datetime = real_time_clock.datetime
    return real_time_clock, clock_battery_low

def initialize_i2c_bus( SCL_pin, SDA_pin, pixel, success_color, fault_color, OFF ):
    try:
        i2c_bus = busio.I2C( SCL_pin, SDA_pin )
        print( "initialized i2c_bus -- 4 green flashes indicate success" )
        for n in range (0, 4):
            pixel.fill( success_color )
            time.sleep( 0.1 )
            pixel.fill( OFF )
            time.sleep( 0.1 )
    except ValueError as err:
        i2c_bus = False
        print( "i2c bus fail: {} -- press reset button, or power off to restart".format( err ))
        while True:
            pixel.fill( fault_color )
            time.sleep( 0.1 )
            pixel.fill( OFF )
            time.sleep( 0.1 )
    return i2c_bus

def initialize_i2c_button( i2c_bus ):
    try:
        button = I2C_Button( i2c_bus )
        print( "initialized button" )
        print("    firmware version", button.version)
        print("    debounce ms", button.debounce_ms)
        button.clear()
        return button
    except:
        print( "button failed to initialize" )
        return False

def initialize_data_file( header, DATA_FILE ):
    try:
        os.stat( DATA_FILE )
        print( "data file already exists, does not need header" )
        return True
    except OSError:
        gc.collect()
        try:
            # create a new data file, and write the header in it
            with open( DATA_FILE, "w" ) as f:
                f.write( header )
            print( "header written" )
            return True
        except OSError as err:
            print( "error opening datafile, {}".format( err ))
            return False

def write_data_to_file( DATA_FILE, datapoint, button, pixel, OFF, success_color, fault_color ):
    try:
        with open( DATA_FILE, "a" ) as f:
            if pixel:
                pixel.fill ( success_color )
            if False: #button:
                button.led_bright = 64
            f.write( datapoint )
            f.write("\n")
            time.sleep( 0.05 )
            f.close()
        if pixel:
            pixel.fill( OFF )
        if False:# button:
            button.led_bright = 1
        return True
    except OSError as err:
        print( "\nError: sd card fail: {:}\n".format(err) )
        if pixel:
            pixel.fill( fault_color ) #  ORANGE to show error: likely no SD card present, or SD card full.
            time.sleep( 0.25 )
            pixel.fill( OFF )
        return False

def initialize_spectral_sensor( i2c_bus ):
    try:
        spectral_sensor = AS7341( i2c_bus )
        print( "spectral sensor initialized" )
        spectral_sensor.led_current = 50
        spectral_sensor.led = True
        time.sleep( 0.5 )
        spectral_sensor.led = False
    except:
        print( "spectral sensor fail" )
        spectral_sensor = False
    return spectral_sensor

def initialize_uva_sensor ( i2c_bus ):
    try:
        uva_sensor = adafruit_ltr390.LTR390( i2c_bus )
        print( "uva sensor initialized" )
    except:
        uva_sensor = False
        print( "uva sensor fail" )
    return uva_sensor

def initialize_sd_card_storage( onboard_neopixel, RED ):
    try:
        cs = board.SD_CS
        sd_card_spi = busio.SPI(board.SD_SCK, MOSI=board.SD_MOSI, MISO=board.SD_MISO)
        sdcard = sdcardio.SDCard(sd_card_spi, cs)
        vfs = storage.VfsFat(sdcard)
        print( "sdcardio success" )
    except( OSError, ValueError ) as err:
        print( "No SD card found, or card is full: {}".format(err) )
        vfs = False
        sdcard = False
        onboard_neopixel.fill( RED )
    return vfs, sdcard

def initialize_neopixel( pin, count ):
    num_pixels = count
    ORDER = neopixel.RGB
    neopixel_instance = neopixel.NeoPixel( pin, num_pixels, brightness=0.3, auto_write=True, pixel_order=ORDER )
    return neopixel_instance

def initialize_led( pin ):
    LED = digitalio.DigitalInOut( pin )
    LED.direction = digitalio.Direction.OUTPUT
    count = 4
    interval = 0.1
    for n in range( 0, count ):
        LED.value = True
        time.sleep(interval)
        LED.value = False
        time.sleep(interval)
    return LED


def memory_check( message, start_mem_free_kB ):
    gc.collect()
    mem_free_kB = gc.mem_free()/1000
    print( "memory check: {}: free memory remaining = {:.2f} kB, {:.2f} %".format( message, mem_free_kB, (100* (mem_free_kB)/start_mem_free_kB )))

def blink(object, count, interval, low_level, high_level):
    try:
        for n in range( 0, count ):
            object.led_bright = high_level
            time.sleep(interval)
            object.led_bright = low_level
            time.sleep(interval)
    except:
        pass

def stall():
    print("intentionally stalled, press return to continue")
    input_string = False
    while input_string == False:
        input_string = input().strip()

mem_free_kB = gc.mem_free()/1000
print("after define functions memory free................. {:.2f} kB, {:.2f} %".format( mem_free_kB, 100 * mem_free_kB/start_mem_free_kB))

main()
