SOFTWARE_VERSION_NUMBER = "1.4.9"
DEVICE_TYPE = "Helio-STELLA"
print( "\n\n{} version {}".format(DEVICE_TYPE, SOFTWARE_VERSION_NUMBER))
# Helio-STELLA instrument
# NASA open source software license
# Paul Mirel 2024

# set the sampling period = reciprocal of the sampling cadence
#               seconds + ( minutes ) + ( hours )
sample_interval_s = 1.0 + ( 0 * 60 ) + ( 0 * 3600 )

## Manual: ##
## 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 visible reference lamp,
## press-and-hold again to turn off the visible reference lamp.

# AS7341 spectral gain settings: 0.5, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512
# *** set gain at line 239 ***

# Sten O: set vis spectrometer cal at line 803
# Sten O: set uv spectrometer cal at line 902

### TBD ###
# use the GPS time to set the real time clock (TBD)
# record high precision GPS

### TBD ###
# AS7331 UV spectrometer gain settings: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048
# set gain with this format: sensor.gain = as7331.GAIN_512X
# UV_spectral_gain = 512
# print( "UV SPECTRAL GAIN = {}".format( UV_spectral_gain ))
# TBD placeholder for magnetic field sensor gain and integration time

# set the welcome display cycle wait time
welcome_wait_s = 0.5#3

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 time
import os
import microcontroller
import board
import digitalio
import rtc
import neopixel
import storage
import sdcardio
import busio
import adafruit_max1704x
from adafruit_pcf8523 import pcf8523
import displayio
import terminalio
import adafruit_displayio_ssd1306
from adafruit_display_text import label
import vectorio
from i2c_button import I2C_Button
from adafruit_as7341 import AS7341,Gain
import adafruit_ltr390
import adafruit_mlx90393
from adafruit_ads1x15.analog_in import AnalogIn
import adafruit_gps
import iorodeo_as7331 as as7331
print( "mem free after imports = {} kB, {} %".format(int(gc.mem_free()/1000), int(100*(gc.mem_free()/1000)/start_mem_free_kB )) )


def main():
    #datapoint_string = ""
    #for x in range (100):
    #    datapoint_string += "placeholder, " ## try to grab a block of memory early
    #print( datapoint_string )
    display = False
    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 )
    onboard_neopixel = initialize_neopixel( board.NEOPIXEL )
    try:
        vfs, sdcard = initialize_sd_card_storage()
        storage.mount(vfs, "/sd")
    except:
        print( "SD card fail: missing or full" )
        vfs = False
        sdcard = False


    success_color = ( 255, 0, 0 ) #Green
    fault_color = ( 102, 255, 0 ) #Orange
    OFF = ( 0, 0, 0 )

    i2c_bus = initialize_i2c_bus( board.SCL, board.SDA )

    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( "found 128x32 display" )
    except:
        display = False
        display_group = False

        # 0x68 -- PCF8523 real time clock
    null_time = time.struct_time(( 2020,  01,   01,   00,  00,  00,   0,   -1,    -1 ))
    try:
        hardware_clock = pcf8523.PCF8523(i2c_bus)
        print( "found hardware_clock" )
        clock_battery_low = hardware_clock.battery_low
        if clock_battery_low:
            print( "    clock battery is low. replace clock battery" )
        else:
            print( "    clock battery is OK" )
        timenow = hardware_clock.datetime
        if timenow.tm_wday not in range ( 0, 7 ):
            hardware_clock.datetime = null_time
        timenow = hardware_clock.datetime
    except (ValueError, NameError) as err:
        print( "hardware clock fail: {}".format(err))
        hardware_clock = False
        clock_battery_low = True
        timenow = null_time
    try:
        system_clock = rtc.RTC()
        system_clock.datetime = hardware_clock.datetime
        print( "    system clock synchronized to pcf8523_real_time_clock" )
    except:
        print( "failed to set system clock" )
    DAYS = { 0:"Sunday", 1:"Monday", 2:"Tuesday",
        3:"Wednesday", 4:"Thursday", 5:"Friday", 6:"Saturday" }

    # create or update the batch number
    batch_number = 0
    if hardware_clock:
        batch_number = update_batch( timenow )
    print( "batch number == {}".format( batch_number ))


    #generate header from hardware online

    # 0x6f -- i2c_button
    try:
        i2c_button = I2C_Button( i2c_bus )
        print( "found i2c button" )
        #print("    firmware version", i2c_button.version)
        #print("    i2c button debounce ms", i2c_button.debounce_ms)
        i2c_button.clear()
    except:
        i2c_button = False

    battery_monitor = initialize_battery_monitor( i2c_bus )
    battery_monitor.report()
    battery_monitor.read()

    # go though the wakeup screens
    # date time wakeup screen

    if display:
        # date time
        if hardware_clock:
            timenow = hardware_clock.datetime
        else:
            timenow = null_time
        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.root_group = 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( welcome_wait_s )

    magnetic_field_sensor = initialize_magnetic_field_sensor( i2c_bus )
    magnetic_field_sensor.report()

    if display:
        # UID/ SD
        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( welcome_wait_s )

    lux_and_uva_sensor = initialize_lux_and_uva_sensor( i2c_bus )
    lux_and_uva_sensor.report()
    uv_spectrometer = initialize_uv_spectrometer( i2c_bus )
    uv_spectrometer.report()

    if display:
        # battery
        display_group.pop()
        display_group.pop()
        text_group = displayio.Group(scale=2, x=0, y=15)
        text = "battery"
        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_monitor.read()
        battery_percent_group = displayio.Group(scale=2, x=90, y=15)
        text = "{}%".format( int( battery_monitor.percentage ))
        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 percent on display" )
        time.sleep( welcome_wait_s )


    gps = initialize_gps( i2c_bus )
    gps.report()
    gps.send_start_commands()
    vis_spectrometer = initialize_vis_spectrometer( i2c_bus )
    vis_spectrometer.report()
    print(vis_spectrometer.swob.gain)
    # uncomment the line with the gain you wish to use
    #vis_spectrometer.swob.gain = Gain.GAIN_0_5X    #returns 0
    #vis_spectrometer.swob.gain = Gain.GAIN_1X    #returns 1
    #vis_spectrometer.swob.gain = Gain.GAIN_2X    #returns 2
    #vis_spectrometer.swob.gain = Gain.GAIN_4X    #returns 3
    #vis_spectrometer.swob.gain = Gain.GAIN_8X    #returns 4
    vis_spectrometer.swob.gain = Gain.GAIN_16X    #returns 5
    #vis_spectrometer.swob.gain = Gain.GAIN_32X    #returns 6
    #vis_spectrometer.swob.gain = Gain.GAIN_64X    #returns 7
    #vis_spectrometer.swob.gain = Gain.GAIN_128X    #returns 8 DEFAULT
    #vis_spectrometer.swob.gain = Gain.GAIN_256X    #returns 9
    #vis_spectrometer.swob.gain = Gain.GAIN_512X    #returns 10
    print(vis_spectrometer.swob.gain)
    #print("gain set to", vis_spectrometer.gain() )
    vis_spectrometer.blink( 0.5 )

    if display:
        # cadence
        display_group.pop()
        display_group.pop()
        text_group = displayio.Group(scale=1, x=0, y=5)
        text = "sample"
        text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        text_group.append(text_area) # Subgroup for text scaling
        display_group.append(text_group)
        text_group = displayio.Group(scale=1, x=0, y=25)
        text = "interval:"
        text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        text_group.append(text_area) # Subgroup for text scaling
        display_group.append(text_group)

        sample_interval_group = displayio.Group(scale=2, x=60, y=15)
        text = "{}s".format( sample_interval_s )
        if sample_interval_s > 59:
            text = "{}m".format( round( sample_interval_s / 60, 1 ))
        if sample_interval_s > 3600:
            text = "{}h".format( round( sample_interval_s / 3600, 1 ))
        interval_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
        sample_interval_group.append(interval_area) # Subgroup for text scaling
        display_group.append(sample_interval_group)
        print( "show interval on display" )
        time.sleep( welcome_wait_s )

    #build the header
    header = "instrument, version, UID, batch, day, iso8601_utc, dec.hour"
    display_state = 0
    ### set display and datafile order here ###
    display_state_names = []
    if vis_spectrometer.swob:
        display_state_names.append ("viscurve")
    if gps.swob:
        header += ", "
        header += gps.header()
        display_state_names.append("gps")
    if magnetic_field_sensor.swob:
        header += ", "
        header += magnetic_field_sensor.header()
        display_state_names.append("magfield")
    if lux_and_uva_sensor.swob:
        header += ", "
        header += lux_and_uva_sensor.header()
        display_state_names.append("luxuva")
    if uv_spectrometer.swob:
        header += ", "
        header += uv_spectrometer.header()
        display_state_names.append("uvspec")
    if vis_spectrometer.swob:
        #display_state_names.append ("viscurve")
        display_state_names.append ("vis_violet_0")
        display_state_names.append ("vis_indigo_1")
        display_state_names.append ("vis_blue_2")
        display_state_names.append ("vis_cyan_3")
        display_state_names.append ("vis_green_4")
        display_state_names.append ("vis_yellow_5")
        display_state_names.append ("vis_orange_6")
        display_state_names.append ("vis_red_7")
        for x in range (8):
            header += ","
            header += vis_spectrometer.header(x)
    if battery_monitor.swob:
        header += ", "
        header += battery_monitor.header()
    header += "\n"
    previous_header = "None"

    # generate filename for today
    fod = ("heliostella_data_{}{:02}{:02}.csv".format(timenow.tm_year, timenow.tm_mon,timenow.tm_mday))
    previous_finu = "placeholder for filename in use"
    previous_header = "placeholder header"
    finu = fod
    # look to see what the previous filename_in_use was
    try:
        with open( "/sd/fn.txt", "r" ) as fn:
            try:
                previous_finu = fn.readline().rstrip()
                finu = previous_finu
            except ValueError:
                previous_finu = "no previous filename"
    except OSError:
            print( "fn.txt file not found, creating new fn.txt file" )
            try:
                with open ( "/sd/fn.txt", "w" ) as fn:
                    fn.write(fod)
            except:
                print( "unable to create fn.txt file")

    try:
        with open( ( "/sd/{}".format( finu )), "r" ) as fn:
            previous_header = fn.readline().rstrip()
            previous_header += "\n"
    except:
        print( "error opening datafile" )

    if previous_finu != finu:
        # create new datafile named finu. write header
        try:
            # create a new data file, and write the header in it
            with open( ( "/sd/{}".format( finu )), "w" ) as f:
                f.write(header)
                #f.write("\n")
            print( "created new data file for today" )
            recording = True
        except OSError as err:
            print( "error opening datafile, {}".format( err ))
            recording = False

    print( len( previous_header))
    print( previous_header )
    print()
    print( len( header ))
    print( header )


    if header == previous_header:
        print( "headers match" )
    else:
        finu = ("heliostella_data_{}{:02}{:02}-{}.csv".format(timenow.tm_year, timenow.tm_mon,timenow.tm_mday,batch_number))
        print( "finu = {}".format( finu ))
        try:
            # create a new data file, and write the header in it
            with open( ( "/sd/{}".format( finu )), "w" ) as f:
                f.write( header )
            print( "created new data file for the new configuration" )
            recording = True
        except OSError as err:
            print( "error opening datafile, {}".format( err ))
            recording = False
        try:
            with open( "/sd/fn.txt", "w" ) as fn:
                fn.write( finu )
        except:
            print( "fn.txt file not available for update" )

    number_of_display_states = len( display_state_names )
    if number_of_display_states < 1:
        display_state_names.append ("None")
        number_of_display_states = len( display_state_names )

    operational = True
    lamp_on = False
    last_button_values = [False, False, False]
    click_allow = True
    click_clear_cycles = 1
    click_loop_count = 0
    loop_count = 0
    interval_start_time_s = time.monotonic()
    gc.collect()
    memory_check( "line 306")
    recording = True
    try:
        battery_monitor.read()
        magnetic_field_sensor.read()
        lux_and_uva_sensor.read()
        uv_spectrometer.read()
        gps.read()
        vis_spectrometer.read()
        if display:
            display_group.pop()
            display_group.pop()
            display_group.pop()
            graph_bar, graph_bar_x, batch_number_label_text_area, polygon = create_graph_screen( display_group )

        ##--#
        if display:
            gc.collect()
            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 )

            luxuva_display_group = displayio.Group()
            lux_label_group = displayio.Group(scale=1, x=5, y=4)
            text = "lux"
            lux_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            lux_label_group.append(lux_label_text_area) # Subgroup for text scaling
            luxuva_display_group.append(lux_label_group)

            lux_value_group = displayio.Group(scale=2, x=5, y=22)
            text = " 0.0"
            lux_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            lux_value_group.append(lux_value_text_area) # Subgroup for text scaling
            luxuva_display_group.append(lux_value_group)

            uva_label_group = displayio.Group(scale=1, x=60, y=4)
            text = "UVA 310nm"
            uva_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            uva_label_group.append(uva_label_text_area) # Subgroup for text scaling
            luxuva_display_group.append(uva_label_group)

            uva_value_group = displayio.Group(scale=2, x=60, y=22)
            text = " 0.0"
            uva_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            uva_value_group.append(uva_value_text_area) # Subgroup for text scaling
            luxuva_display_group.append(uva_value_group)

            #UVspec
            uvspec_display_group = displayio.Group()
            uvcs_label_group = displayio.Group(scale=1, x=5, y=4)
            text = "UVC"
            uvcs_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            uvcs_label_group.append(uvcs_label_text_area) # Subgroup for text scaling
            uvspec_display_group.append(uvcs_label_group)

            uvbs_label_group = displayio.Group(scale=1, x=45, y=4)
            text = "UVB"
            uvbs_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            uvbs_label_group.append(uvbs_label_text_area) # Subgroup for text scaling
            uvspec_display_group.append(uvbs_label_group)

            uvas_label_group = displayio.Group(scale=1, x=85, y=4)
            text = "UVA"
            uvas_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            uvas_label_group.append(uvas_label_text_area) # Subgroup for text scaling
            uvspec_display_group.append(uvas_label_group)

            uvas_value_group = displayio.Group(scale=1, x=85, y=22)
            text = " 0.0"
            uvas_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            uvas_value_group.append(uvas_value_text_area) # Subgroup for text scaling
            uvspec_display_group.append(uvas_value_group)

            uvbs_value_group = displayio.Group(scale=1, x=45, y=22)
            text = " 0.0"
            uvbs_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            uvbs_value_group.append(uvbs_value_text_area) # Subgroup for text scaling
            uvspec_display_group.append(uvbs_value_group)

            uvcs_value_group = displayio.Group(scale=1, x=5, y=22)
            text = " 0.0"
            uvcs_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            uvcs_value_group.append(uvcs_value_text_area) # Subgroup for text scaling
            uvspec_display_group.append(uvcs_value_group)

            gps_display_group = displayio.Group()
            lat_label_group = displayio.Group(scale=1, x=2, y=7)
            text = "Lat N"
            lat_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            lat_label_group.append(lat_label_text_area) # Subgroup for text scaling
            gps_display_group.append(lat_label_group)

            long_label_group = displayio.Group(scale=1, x=60, y=7)
            text = "Long W"
            long_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            long_label_group.append(long_label_text_area) # Subgroup for text scaling
            gps_display_group.append(long_label_group)

            #alt_label_group = displayio.Group(scale=1, x=190, y=4)
            #text = "Alt m"
            #alt_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            #alt_label_group.append(alt_label_text_area) # Subgroup for text scaling
            #gps_display_group.append(alt_label_group)

            #alt_value_group = displayio.Group(scale=1, x=100, y=22)
            #text = " 0"
            #alt_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            #alt_value_group.append(alt_value_text_area) # Subgroup for text scaling
            #gps_display_group.append(alt_value_group)

            lat_value_group = displayio.Group(scale=1, x=2, y=22)
            text = " 0"
            lat_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            lat_value_group.append(lat_value_text_area) # Subgroup for text scaling
            gps_display_group.append(lat_value_group)

            long_value_group = displayio.Group(scale=1, x=60, y=22)
            text = " 0"
            long_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            long_value_group.append(long_value_text_area) # Subgroup for text scaling
            gps_display_group.append(long_value_group)

            mag_display_group = displayio.Group()
            bx_label_group = displayio.Group(scale=1, x=5, y=4)
            text = "Bx"
            bx_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            bx_label_group.append(bx_label_text_area) # Subgroup for text scaling
            mag_display_group.append(bx_label_group)

            by_label_group = displayio.Group(scale=1, x=45, y=4)
            text = "By"
            by_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            by_label_group.append(by_label_text_area) # Subgroup for text scaling
            mag_display_group.append(by_label_group)

            bz_label_group = displayio.Group(scale=1, x=85, y=4)
            text = "Bz   uT"
            bz_label_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            bz_label_group.append(bz_label_text_area) # Subgroup for text scaling
            mag_display_group.append(bz_label_group)

            bx_value_group = displayio.Group(scale=1, x=5, y=22)
            text = " 0"
            bx_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            bx_value_group.append(bx_value_text_area) # Subgroup for text scaling
            mag_display_group.append(bx_value_group)

            by_value_group = displayio.Group(scale=1, x=45, y=22)
            text = " 0"
            by_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            by_value_group.append(by_value_text_area) # Subgroup for text scaling
            mag_display_group.append(by_value_group)

            bz_value_group = displayio.Group(scale=1, x=85, y=22)
            text = " 0"
            bz_value_text_area = label.Label(terminalio.FONT, text=text, color=0xFFFFFF)
            bz_value_group.append(bz_value_text_area) # Subgroup for text scaling
            mag_display_group.append(bz_value_group)
            ##--##


        while operational:
            gc.collect()
            memory_check( "top of the loop" )
            loop_count += 1
            if loop_count > 65535:
                loop_count = 0

            if display:
                show_now = display_state_names[ display_state ]
                if show_now == "viscurve":
                    display.root_group = display_group
                    display_group.hidden = False
                    scaled_graph_data = []
                    for item in vis_spectrometer.irradiance:
                        scaled_graph_data.append(item *1000)
                    graph_data( scaled_graph_data, graph_bar, graph_bar_x, polygon )
                else:
                    display_group.hidden = True


                if ((show_now[:4]) == "vis_"):
                    #print("visc_ display: {}".format(show_now) )
                    display.root_group = waveband_display_group
                    channel = int(show_now[-1])
                    wavelength_text_area.text = str(vis_spectrometer.center_wavelengths_nm[channel])
                    colorname_text_area.text = vis_spectrometer.colors[channel]
                    intensity_text_area.text = str( round(vis_spectrometer.irradiance[channel],3))#str( vis_spectrometer.data[channel] )
                if show_now == "luxuva":
                    display.root_group = luxuva_display_group
                    lux_value_text_area.text = str( int( lux_and_uva_sensor.lux ))
                    uva_value_text_area.text = str( int( lux_and_uva_sensor.UVA ))
                if show_now == "uvspec":
                    display.root_group = uvspec_display_group
                    uvcs_value_text_area.text = str( round( uv_spectrometer.UVC, 3 ))
                    uvbs_value_text_area.text = str( round( uv_spectrometer.UVB, 3 ))
                    uvas_value_text_area.text = str( round( uv_spectrometer.UVA, 3 ))
                if show_now == "magfield":
                    display.root_group = mag_display_group
                    bx_value_text_area.text = str( round( magnetic_field_sensor.Bx, 2 ))
                    by_value_text_area.text = str( round( magnetic_field_sensor.By, 2 ))
                    bz_value_text_area.text = str( round( magnetic_field_sensor.Bz, 2 ))
                if show_now == "gps":
                    display.root_group = gps_display_group
                    if gps.latitude:
                        lat_value_text_area.text = str( round( gps.latitude, 8 ))
                    if gps.longitude:
                        long_value_text_area.text = str( round( gps.longitude, 8 ))
                    #alt_value_text_area.text = str( int( gps.altitude ))

            if i2c_button:
                if not click_allow:
                    if loop_count > click_loop_count + click_clear_cycles:
                        click_allow = True
                        i2c_button.clear()
                        print(" click allow = True")
                button_values = i2c_button.status
                if button_values[ 1 ]:

                    print( "button clicked" )
                    if click_allow:
                        display_state = (display_state + 1) % number_of_display_states
                    i2c_button.led_bright = 16
                    i2c_button.clear()
                else:
                    i2c_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 = i2c_button.status
                        hold_time = time.monotonic() - start_hold_time
                        time.sleep(0.1)
                    i2c_button.clear()
                    if hold_time > 2:
                        click_allow = False
                        lamp_on = not lamp_on
                        if lamp_on:
                            print( "turn on the reference lamp")
                            vis_spectrometer.swob.led = True
                        else:
                            print( "turn off the reference lamp")
                            vis_spectrometer.swob.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
                        i2c_button.led_bright = 16
                        i2c_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()

                battery_monitor.read()
                if battery_monitor.percentage < 25:
                    print ("low battery (<25%)")
                    for i in range (4):
                        onboard_blue_led.value = True
                        time.sleep(0.1)
                        onboard_blue_led.value = False
                        time.sleep(0.1)
                magnetic_field_sensor.read()
                lux_and_uva_sensor.read()
                uv_spectrometer.read()
                gps.read()
                vis_spectrometer.read()

                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 ]
                gc.collect()

                try:
                    with open( "/sd/{}".format( finu ), "a" ) as f:
                        if onboard_neopixel:
                            onboard_neopixel.fill ( success_color )
                        f.write( "{}, {}, ".format( DEVICE_TYPE, SOFTWARE_VERSION_NUMBER) )
                        f.write( "{}, {}, {}, {}, {}".format( UID, batch_number, weekday, iso8601_utc, decimal_hour ))
                        if gps.swob:
                            f.write( ", " )
                            f.write( gps.log())
                        if magnetic_field_sensor.swob:
                            f.write( ", " )
                            f.write( magnetic_field_sensor.log())
                        if lux_and_uva_sensor.swob:
                            f.write( ", " )
                            f.write( lux_and_uva_sensor.log() )
                        if uv_spectrometer.swob:
                            f.write( ", " )
                            f.write( uv_spectrometer.log())
                        if vis_spectrometer.swob:
                            for ch in range(8):
                                f.write( ", " )
                                f.write( vis_spectrometer.log(ch))
                        if battery_monitor.swob:
                            f.write( ", " )
                            f.write( battery_monitor.log())
                        f.write("\n")
                        time.sleep( 0.05 )
                        f.close()
                    if onboard_neopixel:
                        onboard_neopixel.fill( OFF )
                except OSError as err:
                    print( "\nError: sd card fail: {:}".format(err) )
                    print( "*** The card may be missing, or you may need to create a folder named sd in the root directory of CIRCUITPY ***" )
                    if onboard_neopixel:
                        onboard_neopixel.fill( fault_color ) #  fault_color to show error: likely no SD card present, or SD card full.
                        time.sleep( 1 )
                        onboard_neopixel.fill( OFF )
                    recording = False

                ### set print to serial order here ###
                if False:
                    gps.printlog()
                    magnetic_field_sensor.printlog()
                    lux_and_uva_sensor.printlog()
                    uv_spectrometer.printlog()
                    for ch in range( 8 ):
                        vis_spectrometer.printlog(ch)
                    battery_monitor.printlog()

    finally:  # clean up on exit
        if sdcard:
            sdcard.deinit()
            print( "sdcard deinitialized" )
        if i2c_bus:
            i2c_bus.deinit()
            print( "i2c_bus deinitialized" )
        if display:
            displayio.release_displays()
            print( "displayio displays released" )

class Device: #parent class
    def __init__(self, name = None, pn = None, address = None, swob = None):
        self.name = name
        self.swob = swob
        self.pn = pn
        self.address = address
    def report(self):
        if self.swob is not None:
            print("report:", self.name, self.pn, hex(self.address), "found" )

def initialize_vis_spectrometer( i2c_bus ):
    vis_spectrometer = Null_Vis_Spectrometer()
    try:
        vis_spectrometer = as7341_Vis_Spectrometer( i2c_bus )
    except:
        pass
    return vis_spectrometer

class as7341_Vis_Spectrometer( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "vis_spectrometer", pn = "as7341", address = 0x39, swob = AS7341( com_bus ))
        self.center_wavelengths_nm = [415, 445, 480, 515, 555, 590, 630, 680]
        self.bandwidths_nm = [26, 30, 36, 39, 39, 40, 50, 52]
        self.colors = ["violet", "indigo", "blue", "cyan", "green", "yellow", "orange", "red"]
        #self.tsis_cal_counts_per_irradiance = 1405.9, 2079.6, 2631.6, 3556.8, 4246.0, 5060.6, 6888.9, 9130.9
        self.steno_cal_counts_per_irradiance = 4398.0, 6104.0, 7583.0, 9972.0, 11536.0, 13374.0, 17115.0, 20916.0

        self.calibration_error = 0.6
        self.irradiance = [0,0,0,0,0,0,0,0]
    def blink( self, duration ):
        self.swob.led_current = 50
        self.swob.led = True
        time.sleep( duration )
        self.swob.led = False
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        self.data = self.swob.all_channels
    def list_channels():
        return self.center_wavelengths
    def header(self, ch):
        return " {}.WL.nm, {}.counts, {}.W/(m^2*nm), {}.uncty.W/(m^2*nm)".format( self.colors[ch], self.colors[ch], self.colors[ch], self.colors[ch] )
    def log(self, ch ):
        #self.irradiance[ch] = self.data[ch]/self.tsis_cal_counts_per_irradiance[ch]
        self.irradiance[ch] = self.data[ch]/self.steno_cal_counts_per_irradiance[ch]
        return "{}, {}, {}, {}".format( self.center_wavelengths_nm[ch], self.data[ch],
            self.irradiance[ch], self.irradiance[ch]*self.calibration_error )
    def printlog(self,ch):
        print( self.log(ch))

class Null_Vis_Spectrometer():
    def __init__( self ):
        self.swob = None
    def found(self):
        pass
    def read(self):
        pass
    def log(self):
        pass
    def report(self):
        pass
    def printlog(self):
        pass
    def blink(self, duration):
        pass
    def header(self):
        pass

def initialize_gps( i2c_bus ):
    gps = Null_GPS()
    try:
        gps = pa1010d_GPS( i2c_bus )
    except:
        pass
    return gps

class pa1010d_GPS( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "gps", pn = "pa1010d", address = 0x10, swob = adafruit_gps.GPS_GtopI2C( com_bus, debug=False ))
    def found(self):
        print("found", self.pn, self.swob)
    def send_start_commands(self):
        self.swob.send_command(b"PMTK314,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0") #set data output configuration
        self.swob.send_command(b"PMTK220,1000") #set update interval to 1000 ms
    def read(self):
        self.swob.update()
        self.latitude = self.swob.latitude
        self.lat_deg = self.swob.latitude_degrees
        self.lat_min = self.swob.latitude_minutes
        self.longitude = self.swob.longitude
        self.long_deg = self.swob.longitude_degrees
        self.long_min = self.swob.longitude_minutes
        self.altitude = self.swob.altitude_m
    def header(self):
        return( "lat.deg.N, long.deg.W, alt.m" )
    def log(self):
        return "{}, {}, {}".format( self.latitude, self.longitude, self.altitude )
    def printlog(self):
        print( self.log())

class Null_GPS():
    def __init__( self ):
        self.swob = None
    def found(self):
        pass
    def read(self):
        pass
    def log(self):
        pass
    def report(self):
        pass
    def send_start_commands(self):
        pass
    def printlog(self):
        pass
    def header(self):
        pass

def initialize_uv_spectrometer( i2c_bus ):
    uv_spectrometer = Null_Lux_And_UVA_Sensor()
    try:
        uv_spectrometer = as7331_UV_Spectrometer( i2c_bus )
    except:
        pass
    return uv_spectrometer

class as7331_UV_Spectrometer( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "uv_spectrometer", pn = "as7331", address = 0x74, swob = as7331.AS7331( com_bus ))
        self.steno_cal_raw_per_irradiance = 240, 1680, 7771 #UVC, UVB, UVA
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        self.UVA, self.UVB, self.UVC, self.temperature = self.swob.values
    def header(self):
        return "UVC.WL.nm, UVC_raw, UVC_W/(m^2*nm), UVB.WL.nm, UVB_raw, UVB_W/(m^2*nm), UVA.WL.nm, UVA_raw, UVA_W/(m^2*nm), UVS.temp.C"
    def log(self):
        return "260, {}, {}, 300, {}, {}, 360, {}, {}, {}".format( self.UVC,
            self.UVC/self.steno_cal_raw_per_irradiance[0],
            self.UVB, self.UVB/self.steno_cal_raw_per_irradiance[1],
            self.UVA, self.UVA/self.steno_cal_raw_per_irradiance[2], self.temperature )
    def printlog(self):
        print( self.log())

class Null_UV_spectrometer():
    def __init__( self ):
        self.swob = None
    def found(self):
        pass
    def read(self):
        pass
    def log(self):
        pass
    def report(self):
        pass
    def printlog(self):
        pass
    def header(self):
        pass

def initialize_lux_and_uva_sensor( i2c_bus ):
    lux_and_uva_sensor = Null_Lux_And_UVA_Sensor()
    try:
        lux_and_uva_sensor = ltr390_Lux_And_UVA_Sensor( i2c_bus )
    except:
        pass
    return lux_and_uva_sensor

class ltr390_Lux_And_UVA_Sensor( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "lux_and_uva_sensor", pn = "ltr390", address = 0x53, swob = adafruit_ltr390.LTR390( com_bus ))
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        #MX, MY, MZ = SENSOR.magnetic
        self.UVA = self.swob.uvs
        self.lux = self.swob.lux
    def header(self):
        return "illum.lux, UVA.WL.nm, UVA_uncal"
    def log(self):
        return "{}, 320, {}".format( self.lux, self.UVA )
    def printlog(self):
        print( self.log())

class Null_Lux_And_UVA_Sensor():
    def __init__( self ):
        self.swob = None
    def found(self):
        pass
    def read(self):
        pass
    def log(self):
        pass
    def report(self):
        pass
    def printlog(self):
        pass
    def header(self):
        pass

def initialize_magnetic_field_sensor( i2c_bus ):
    magnetic_field_sensor = Null_Magnetic_Field_Sensor()
    try:
        magnetic_field_sensor = mlx90393_Magnetic_Field_Sensor( i2c_bus )
    except:
        pass
    return magnetic_field_sensor

class mlx90393_Magnetic_Field_Sensor( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "magnetic_field_sensor", pn = "mlx90393", address = 0x18, swob = adafruit_mlx90393.MLX90393( com_bus, gain=adafruit_mlx90393.GAIN_1X ))
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        #MX, MY, MZ = SENSOR.magnetic
        self.Bx, self.By, self.Bz = self.swob.magnetic
    def header(self):
        return "Bx.uT, By.uT, Bz.uT"
    def log(self):
        return "{}, {}, {}".format( self.Bx, self.By, self.Bz, )
    def printlog(self):
        print( self.log())

class Null_Magnetic_Field_Sensor():
    def __init__( self ):
        self.swob = None
    def found(self):
        pass
    def read(self):
        pass
    def log(self):
        pass
    def report(self):
        pass
    def printlog(self):
        pass
    def header(self):
        pass

def initialize_battery_monitor( i2c_bus ):
    battery_monitor = Null_Battery_Monitor()
    try:
        battery_monitor = max1704x_Battery_Monitor( i2c_bus )
    except:
        pass
    return battery_monitor

class max1704x_Battery_Monitor( Device ): #child class ( parent class ):
    def __init__( self, com_bus ):
        super().__init__(name = "battery_monitor", pn = "max1704x", address = 0x36, swob = adafruit_max1704x.MAX17048( com_bus ))
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        self.voltage = self.swob.cell_voltage
        self.percentage = self.swob.cell_percent
    def header(self):
        return "battery_voltage, battery_percent"
    def log(self):
        return "{}, {}".format( self.voltage, self.percentage )
    def printlog(self):
        print( self.log())

class Null_Battery_Monitor():
    def __init__( self ):
        self.swob = None
    def found(self):
        pass
    def read(self):
        pass
    def log(self):
        pass
    def report(self):
        pass
    def printlog(self):
        pass
    def header(self):
        pass

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 ):
    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( 8 ):
            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 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 )
            b.write("\n")
    except OSError as err:
        print("Error: writing batch.txt {:}".format(err) )
        pass
    batch_string = ( "{:}".format( batch_number ))
    return batch_string

def request_firmware_report( gps_sensor ):
    gps_sensor.send_command(b"PMTK605")  # request firmware version
    data = gps_sensor.read(32)  # read up to 32 bytes
        # print(data)  # this is a bytearray type
    if data is not None:
        # convert bytearray to string
        data_string = "".join([chr(b) for b in data])
        #print( "gps firmware report = ", end='')
        report = data_string[:4]
        #print(report, end="")
    else:
        report = None
        print( "no data on firmware" )
    #print("\n")
    return report

def initialize_i2c_bus( SCL_pin, SDA_pin ):
    try:
        i2c_bus = busio.I2C( SCL_pin, SDA_pin )
        print( "initialized i2c_bus" )
    except ValueError as err:
        i2c_bus = False
        print( "i2c bus fail: {} -- press reset button, or power off to restart".format( err ))
    return i2c_bus

def initialize_sd_card_storage( ):
    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( 255,0,0 )
    return vfs, sdcard

def initialize_neopixel( pin ):
    num_pixels = 1
    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
    LED.value = True
    time.sleep(interval)
    LED.value = False
    return LED

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 memory_check( message ):
    gc.collect()
    mem_free_kB = gc.mem_free()/1000
    print( "{} memory free: {} kB, {} %".format( message, int(mem_free_kB), int((100* (mem_free_kB)/start_mem_free_kB ))))

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

gc.collect()
main()
