SOFTWARE_VERSION_NUMBER = "0.2.3"
DEVICE_TYPE = "STELLA-1.2"
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 = 2.0 + ( 0 * 60 ) + ( 0 * 3600 )
burst_count = 1 #3 ( burst_count of 0 takes no data )
ch4_if_co2 = True # set ch4 sensor as present if co2 sensor is present.

# set the welcome display cycle wait time
welcome_wait_s = 1#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 vectorio
import time
import terminalio
import storage
import sdcardio
import rtc
import rotaryio
import os
import neopixel
import microcontroller
import displayio
import digitalio
import busio
import board
import adafruit_max1704x
import adafruit_ltr390
import adafruit_ili9341
import adafruit_gps
import adafruit_focaltouch
import adafruit_displayio_ssd1306
from analogio import AnalogIn
from adafruit_pcf8523 import pcf8523
from adafruit_display_text import label
from adafruit_bme280 import basic as adafruit_bme280
import AS7265X_sparkfun
from AS7265X_sparkfun import AS7265X
import adafruit_mlx90614
import adafruit_mcp9808
import adafruit_vl53l1x
import adafruit_scd4x
from adafruit_pm25.i2c import PM25_I2C
import iorodeo_as7331 as as7331
import math

print( "mem free after imports = {} kB, {} %".format(int(gc.mem_free()/1000), int(100*(gc.mem_free()/1000)/start_mem_free_kB )) )


def main():
    ch4_if_co2 = True # if the co2 sensor is online, assume the ch4 sensor is too. this is incorrect, but better than nothing
    displayio.release_displays()
    UID = int.from_bytes(microcontroller.cpu.uid, "big") % 10000
    print("unique identifier (UID) : {0}".format( UID ))

    #initialize system devices
    onboard_red_led = initialize_led( board.LED )
    onboard_neopixel = initialize_neopixel( board.NEOPIXEL )

    #initialize system busses, storage, display, touch_screen, encoder
    spi_bus = initialize_spi_bus()
    vfs, sdcard = initialize_sd_card_storage( spi_bus, onboard_neopixel )
    i2c_bus = initialize_i2c_bus( board.SCL, board.SDA )
    GPS_uart = initialize_uart( board.TX, board.RX )
    display = initialize_display( spi_bus )
    welcome_group = create_welcome_screen( display )
    display.root_group = welcome_group
    while not i2c_bus.try_lock():
        time.sleep(0.1)
    i2c_scan_results = [hex(device_address) for device_address in i2c_bus.scan()]
    i2c_bus.unlock()
    print ( i2c_scan_results )
    touch_screen = initialize_touch_screen( i2c_bus )
    touch_screen.report()
    encoder = initialize_encoder(board.A5, board.A4)
    last_encoder_position = None
    encoder_button = initialize_encoder_button( board.D5 )
    #initialize hardware clock, sync system clock
    hardware_clock = initialize_hardware_clock( i2c_bus )

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

    # check which sensors are present. hardware clock already initialized.
    # high_accuracy_air_temperature
    # co2
    # particulates
    # don't know how to test for methane sensor present.

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

    # gps
    gps = initialize_gps( GPS_uart )
    gps.report()
    gps.send_start_commands()

    # ambient_air
    ambient_air_sensor = initialize_ambient_air_sensor( i2c_bus )
    ambient_air_sensor.report()

    # uv spectrometer
    uv_spectrometer = initialize_uv_spectrometer( i2c_bus )
    uv_spectrometer.report()

    # vis_nir_triad_spectrometer
    vis_nir_triad_spectrometer = initialize_vis_nir_triad_spectrometer( i2c_bus )
    vis_nir_triad_spectrometer.report()

    # tir_surface_thermometer
    tir_surface_thermometer = initialize_tir_surface_thermometer( i2c_bus )
    tir_surface_thermometer.report()

    print( "if we hang up here, it's likely due to an unhandled pystack exhausted error." )
    # lidar_4m_range #pystack ehausted error.
    # see https://github.com/adafruit/circuitpython/pull/8598
    # seems to work best without a start heap size setting in the toml file.
    # only one line: CIRCUITPY_PYSTACK_SIZE=4000
    if True:
        try:
            lidar_4m_range = initialize_lidar_4m_range( i2c_bus )
            print( "report: lidar found" )
        except (RuntimeError, ValueError) as err:
            print( "lidar failed to initialize: {}".format( err ))
            lidar_4m_range = False

        #lidar_4m_range.report()


    # high_accuracy_air_thermometer
    high_accuracy_air_thermometer = initialize_high_accuracy_air_thermometer( i2c_bus )
    high_accuracy_air_thermometer.report()

    # co2
    co2_photoacoustic = initialize_co2_photoacoustic( i2c_bus )
    co2_photoacoustic.report()

    # particulates
    # check for address 0x12, if not present, send False
    # TBD handle this exception in the class definition
    if '0x12' in i2c_scan_results:
        particulates_counter = initialize_particulates_counter( i2c_bus )
    else:
        particulates_counter = initialize_particulates_counter( False )
    particulates_counter.report()

    boost_5V = initialize_output_pin( board.D6 )
    monitor_5V = AnalogIn(board.A2)
    spare_analog = AnalogIn(board.A0)
    # TBD see if we can put some of this in a class
    ch4_chemoresistor_ppm = None
    if co2_photoacoustic.swob and ch4_if_co2:
        # methane
        ch4_chemoresistor = True
        ch4_signal = AnalogIn(board.A1)
        # power up the 5V, start the timer need 48h bake-in, and then 15m warmup each use
        boost_5V.value = True
        ch4_timer_start = time.monotonic
        print( "ch4 input initialized" )
    else:
        ch4_chemoresistor_ppm = None
        ch4_chemoresistor = False
        boost_5V.value = False
        print( "turn off the 5V supply if not in use" )
    #boost_5V.value = False
    print( "end sensor initializations\n" )

    # create the date stamp from timenow
    datestamp = "{}{:02}{:02}".format(timenow.tm_year, timenow.tm_mon,timenow.tm_mday)
    #print( datestamp)

    ### begin point data file handling ###
    # create the point_header
    point_header = []
    point_header = "iso8601_utc, decimal_hour, UID, batch, burst_mmt"

    # TBD handle all this questioning in the class. header should return the right stuff, null header should return an empty string ""
    if gps.swob:
        point_header += ",  lat.deg.N, long.deg.W, alt.m.gps, iso8601_gps"
    if lidar_4m_range:#.swob:
        point_header += ", range.m"
    if tir_surface_thermometer.swob:
        point_header += ", surf.temp.C"
    if high_accuracy_air_thermometer.swob:
        point_header += ", ha.air.temp.C"
    if ambient_air_sensor.swob:
        point_header += ", amb.air.temp.C, amb.air.humid.percent, amb.air.dewpt.C, amb.air.press.hPa, amb.press_alt.m"
    if co2_photoacoustic.swob:
        point_header += ", co2.photoacoustic.ppm, co2.photoacoustic.uncty.ppm, co2.sensor.temp.C, co2.sensor.humid.percent"
        if ch4_if_co2:
            point_header += ", ch4.raw.counts, ch4.ppm, 5V.supply.V"
    if battery_monitor.swob:
        point_header += ", bat.V, bat.percent"
    if particulates_counter.swob:
        point_header += ", particulates.aqi_p, particulates.1um_ug.per.m.cu"	
        point_header += ", particulates.2.5um_ug.per.m.cu, particulates.10um_ug.per.m.cu"
        point_header += ", particulates.0.3um_ct.per.100mL, particulates.0.5um_ct.per.100mL"
        point_header += ", particulates.1um_ct.per.100mL, particulates.2.5um_ct.per.100mL"
        point_header += ", particulates.10um_ct.per.100mL, particulates.100um_ct.per.100mL"
    point_header += "\n"
    #print( point_header )

    # try open the pfn.txt file
    previous_point_filename = "stella-1.2_data_20000101_point.csv"
    try:
        with open( "/sd/pfn.txt", "r" ) as pfn:
            try:
                previous_point_filename = pfn.readline().rstrip()
                #print("previous_point_filename =", previous_point_filename)
            except ValueError:
                previous_point_filename = False
    except OSError:
            print( "file not found, creating new pfn.txt file")
            try:
                with open( "/sd/pfn.txt", "w" ) as pfn:
                    previous_point_filename = "stella-1.2_data_20000101_point.csv"
                    pfn.write( previous_point_filename )
            except:
                print( "unable to create pfn.txt file")
    previous_point_file_date = previous_point_filename[16:24]
    #print( previous_point_file_date )
    if datestamp == previous_point_file_date:
        #print( "same day" )
        point_filename_in_use = previous_point_filename
    else:
        print( "different day, start a new point data file" )
        point_filename_in_use = ("stella-1.2_data_{}_point.csv".format( datestamp ))
        try:
            with open( "/sd/pfn.txt", "w" ) as pfn:
                pfn.write( point_filename_in_use )
        except:
            print( "unable to modify pfn.txt file")
    #print( point_filename_in_use )
    # open/create the point data file using point_filename_in_use
    previous_point_header = point_header
    try:
        with open( "/sd/{}".format( point_filename_in_use ), "r" ) as pfniu:
            try:
                previous_point_header = pfniu.readline().rstrip()
                #print("previous_point_header =", previous_point_header)
            except ValueError:
                previous_point_header = False
    except OSError:
            print( "file not found" )#, creating new point data file")
            previous_point_header = point_header
            #try:
            #    with open( "/sd/{}".format( point_filename_in_use ), "w" ) as pfniu:
            #        previous_point_header = point_header
            #        pfniu.write( point_header )
            #except:
            #    print( "unable to create point data file")

    if point_header[:-1] == previous_point_header: #remove the trailing \n for comparison
        pass
        print( "point headers match, continue using existing file" )
    else:
        print( "point headers do not match, create a new daily file with batch suffix" )
        point_filename_in_use = ("stella-1.2_data_{}{:02}{:02}_{}_point.csv".format(timenow.tm_year, timenow.tm_mon,timenow.tm_mday, batch_number))
        try:
            with open( "/sd/{}".format(point_filename_in_use), "w" ) as pfniu:
                pfniu.write(point_header)
        except:
            print( "unable to create point data file")
        try:
            with open( "/sd/pfn.txt", "w" ) as pfn:
                pfn.write( point_filename_in_use )
        except:
            print( "unable to modify pfn.txt file")
    ### end point data file handling ###

    ### begin spectral data file handling ###
    spectrometry = False
    if uv_spectrometer.swob or vis_nir_triad_spectrometer.swob:
        spectrometry = True

    if spectrometry:
        # header is the same for any/ all spectrometer sensors
        spectral_header = []
        spectral_header = "iso8601_utc, decimal_hour, UID, batch, burst_mmt, range_m"
        spectral_header += ", sensor.pn, WL.nm, raw.counts"
        spectral_header += ", irrad_uw.per.cm.sq_stella_cal, irrad.uncty_uw.per.cm.sq_stella_cal"
        spectral_header += ", irrad_uw.per.cm.sq_factory_cal, irrad.uncty_uw.per.cm.sq_factory_cal"
        spectral_header += ", gain, integration.time_ms, bw_fwhm_nm, afov_deg, chip.temp_C\n"

        # try open the pfn.txt file
        previous_spectral_filename = "stella-1.2_data_20000101_spectral.csv"
        try:
            with open( "/sd/sfn.txt", "r" ) as sfn:
                try:
                    previous_spectral_filename = sfn.readline().rstrip()
                    #print("previous_spectral_filename =", previous_spectral_filename)
                except ValueError:
                    previous_spectral_filename = False
        except OSError:
                print( "file not found, creating new sfn.txt file")
                try:
                    with open( "/sd/sfn.txt", "w" ) as sfn:
                        previous_spectral_filename = "stella-1.2_data_20000101_spectral.csv"
                        sfn.write( previous_spectral_filename )
                except:
                    print( "unable to create sfn.txt file")
        previous_spectral_file_date = previous_spectral_filename[16:24]
        #print( previous_spectral_file_date )
        if datestamp == previous_spectral_file_date:
            #print( "same day" )
            spectral_filename_in_use = previous_spectral_filename
        else:
            print( "different day, start a new spectral data file" )
            spectral_filename_in_use = ("stella-1.2_data_{}_spectral.csv".format( datestamp ))
            try:
                with open( "/sd/sfn.txt", "w" ) as sfn:
                    sfn.write( spectral_filename_in_use )
            except:
                print( "unable to modify sfn.txt file")
        #print( spectral_filename_in_use )
        # open/create the spectral data file using spectral_filename_in_use
        try:
            with open( "/sd/{}".format( spectral_filename_in_use ), "r" ) as sfniu:
                try:
                    previous_spectral_header = sfniu.readline().rstrip()
                    #print("previous_spectral_header =", previous_spectral_header)
                except ValueError:
                    previous_spectral_header = False
        except OSError:
                print( "file not found, creating new spectral data file")
                try:
                    with open( "/sd/{}".format( spectral_filename_in_use ), "w" ) as sfniu:
                        previous_spectral_header = spectral_header
                        sfniu.write( spectral_header )
                except:
                    print( "unable to create spectral data file")
        ### end spectral data file handling ###

    time.sleep(1) #hold welcome screen

    highlight = []

    ### begin graphics

    pal = displayio.Palette(20)
    pal[0] = 0x000000 # black
    pal[1] = 0xA0522D # brown
    pal[2] = 0xFF0000 # red
    pal[3] = 0xFF8C00 # orange
    pal[4] = 0xFFFF00 # yellow
    pal[5] = 0x00FF00 # green
    pal[6] = 0x0000FF # blue
    pal[7] = 0x9400D3 # violet
    pal[8] = 0x808080 # grey
    pal[9] = 0xFFFFFF # white
    pal[10] = 0xFF99FF # light
    pal[11] = 0xFF751A # heat
    pal[12] = 0x66CCFF # air
    pal[13] = 0x6FDC6F # plants
    pal[14] = 0xDEB887 # here
    pal[15] = 0xD3D3D3 # sensors
    pal[16] = 0x00FFEE # burst
    pal[17] = 0x0066FF # border
    pal[18] = 0x009900 # GPS flag

    # create top control bar
    print("create top_bar")
    main_display_group = displayio.Group()
    del welcome_group

    # make a highlight border color full screen rectangle
    border = vectorio.Rectangle(pixel_shader=pal, color_index=17, width=320, height=int(240), x=0, y=0)
    main_display_group.append( border )

    # make a white border revealing rectangle
    border_width = 5
    border_reveal = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=320-2*border_width, height=240-2*border_width, x=0+border_width, y=0+border_width)
    main_display_group.append( border_reveal )

    # make a white full screen rectangle
    background = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=320, height=int(240), x=0, y=0)
    main_display_group.append( background )
    background.hidden = False#True

    #mm_bitmap = displayio.OnDiskBitmap("/lib/nasa_320x240.bmp")
    #mm_tile_grid = displayio.TileGrid(mm_bitmap, pixel_shader=mm_bitmap.pixel_shader)
    #main_menu_group.append(mm_tile_grid)

    display.root_group = main_display_group

    # put in GPS nofix/FIX upper left
    gps_icon_y = 14
    gps_fix_flag_rect = vectorio.Rectangle(pixel_shader=pal, color_index=18, width=54, height=20, x=3, y=gps_icon_y)
    main_display_group.append( gps_fix_flag_rect )
    gps_fix_flag_rect.hidden = True
    gps_fix_group = displayio.Group(scale=1, x=8, y=gps_icon_y+9)
    gps_fix_text = "GPS:nofix"
    gps_fix_text_area = label.Label(terminalio.FONT, text=gps_fix_text, color=0x000000)
    gps_fix_group.append(gps_fix_text_area) # Subgroup for text scaling
    main_display_group.append(gps_fix_group)

    # put in batch text
    batch_icon_y = 10
    bt_border_rect = vectorio.Rectangle(pixel_shader=pal, color_index=0, width=50, height=28, x=68, y=batch_icon_y)
    main_display_group.append( bt_border_rect )

    bt_bar_rect = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=46, height=24, x=70, y=batch_icon_y+2)
    main_display_group.append( bt_bar_rect )

    bt_group = displayio.Group(scale=2, x=76, y=14+batch_icon_y)
    bt_text = "{}".format( batch_number )
    bt_text_area = label.Label(terminalio.FONT, text=bt_text, color=0x000000)
    bt_group.append(bt_text_area) # Subgroup for text scaling
    main_display_group.append(bt_group)

    btt_group = displayio.Group(scale=1, x=76, y=36+batch_icon_y)
    btt_text = "batch"
    btt_text_area = label.Label(terminalio.FONT, text=btt_text, color=0x000000)
    btt_group.append(btt_text_area) # Subgroup for text scaling
    main_display_group.append(btt_group)

    # add park icon for rotary menu control
    highlight.append( vectorio.Rectangle(pixel_shader=pal, color_index=0, width=10, height=10, x=124, y=14+batch_icon_y) )
    main_display_group.append( highlight[0] )
    highlight[0].hidden = True

    # put in Bat% upper right
    battery_icon_y = 18
    bat_bar_frame_rect = vectorio.Rectangle(pixel_shader=pal, color_index=0, width=40, height=20, x=261, y=battery_icon_y)
    main_display_group.append( bat_bar_frame_rect )
    bat_bar_rect = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=36, height=16, x=263, y=battery_icon_y+2)
    main_display_group.append( bat_bar_rect )
    # add battery nose
    bat_nose_frame_rect = vectorio.Rectangle(pixel_shader=pal, color_index=0, width=5, height=10, x=301, y=battery_icon_y+5)
    main_display_group.append( bat_nose_frame_rect )
    bat_nose_rect = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=5, height=6, x=299, y=battery_icon_y+7)
    main_display_group.append( bat_nose_rect )
    # add battery segments
    number_of_bat_segments = 9
    bat_segment_x = []
    for n in range (0,number_of_bat_segments):
        bat_segment_x.append(265+4*n)
    bat_segment = []
    for n in range (0,number_of_bat_segments):
        bat_segment.append(vectorio.Rectangle(pixel_shader=pal, color_index=0, width=4, height=12, x=bat_segment_x[n], y=battery_icon_y+4))
    for n in range (0,number_of_bat_segments-1):
        main_display_group.append(bat_segment[n])

    # add record/ pause icons and add highlight border
    rp_icon_y = 0
    highlight.append( vectorio.Rectangle(pixel_shader=pal, color_index=0, width=56, height=56, x=132, y=rp_icon_y))
    main_display_group.append( highlight[1] )
    highlight[1].hidden = True
    rp_bg = vectorio.Rectangle(pixel_shader= pal, color_index=9, width=46, height=46, x=137, y=rp_icon_y+5)
    main_display_group.append( rp_bg )
    rp_pause = vectorio.Rectangle(pixel_shader= pal, color_index=0, width=27, height=27, x=147, y=rp_icon_y+15)
    main_display_group.append( rp_pause )
    rp_pause_split = vectorio.Rectangle(pixel_shader= pal, color_index=9, width=9, height=27, x=156, y=rp_icon_y+15)
    main_display_group.append( rp_pause_split )
    record_circle = vectorio.Circle( pixel_shader=pal, color_index=2, radius=19, x=159, y=rp_icon_y+28 )
    main_display_group.append( record_circle )
    record_circle.hidden = False

    highlight.append( vectorio.Rectangle(pixel_shader= pal, color_index=0, width=56, height=56, x=196, y=rp_icon_y))
    main_display_group.append( highlight[2] )
    highlight[2].hidden = True

    burst_bg = vectorio.Rectangle(pixel_shader= pal, color_index=9, width=46, height=46, x=201, y=rp_icon_y+5)
    main_display_group.append( burst_bg )
    burst_circle = vectorio.Circle( pixel_shader=pal, color_index=16, radius=22, x=224, y=rp_icon_y+28 )
    main_display_group.append( burst_circle )

    burst_less_than_ten_x = 219
    burst_ten_or_greater_x = 213
    burst_group = displayio.Group(scale=2, x=213, y=rp_icon_y+27)
    if burst_count < 10:
        burst_group.x = burst_less_than_ten_x
    else:
        burst_group.x = burst_ten_or_greater_x
    burst_text = "{}".format( burst_count )
    burst_text_area = label.Label(terminalio.FONT, text=burst_text, color=0x000000)
    burst_group.append(burst_text_area) # Subgroup for text scaling
    main_display_group.append(burst_group)

    ### end common screen group ###
    print("create home menu")

    mm_number_of_columns = 2
    mm_number_of_rows = 3
    mm_start_x = 10
    mm_spacing_x = 150
    mm_start_y = 56
    mm_spacing_y = 60
    mm_item_border_rects = []
    mm_item_rects = []

    for j in range (0,mm_number_of_rows):
        for i in range (0, mm_number_of_columns):
            x_in_use = mm_start_x+i*mm_spacing_x
            y_in_use = mm_start_y+j*mm_spacing_y
            highlight.append(vectorio.Rectangle(pixel_shader=pal, color_index=0, width=150,
                                                height=55, x=x_in_use,
                                                y=y_in_use))
            mm_item_border_rects.append(vectorio.Rectangle(pixel_shader=pal, color_index=0, width=134,
                                                height=39, x=x_in_use+8, y=y_in_use+8))
            mm_item_rects.append(vectorio.Rectangle(pixel_shader=pal, color_index=9, width=130,
                                                height=35, x=x_in_use+10, y=y_in_use+10))



    home_station_group = displayio.Group()
    for n in range( 3, len(highlight)  ):
        home_station_group.append( highlight[n] )
        highlight[n].hidden = True

    for n in range( 0, len(mm_item_border_rects)  ):
        home_station_group.append( mm_item_border_rects[n] )
        home_station_group.append( mm_item_rects[n] )

    main_display_group.append( home_station_group )

    for i in range(0,6):
        mm_item_rects[i].color_index = i+10

    first_text_group = displayio.Group(scale=2, x=30, y=83)
    first_text = "Light"
    first_text_area = label.Label(terminalio.FONT, text=first_text, color=0x000000)
    first_text_group.append(first_text_area) # Subgroup for text scaling
    home_station_group.append(first_text_group)

    second_text_group = displayio.Group(scale=2, x=184, y=83)
    second_text = "Heat"
    second_text_area = label.Label(terminalio.FONT, text=second_text, color=0x000000)
    second_text_group.append(second_text_area) # Subgroup for text scaling
    home_station_group.append(second_text_group)

    third_text_group = displayio.Group(scale=2, x=30, y=83+60*1)
    third_text = "Air"
    third_text_area = label.Label(terminalio.FONT, text=third_text, color=0x000000)
    third_text_group.append(third_text_area) # Subgroup for text scaling
    home_station_group.append(third_text_group)

    fourth_text_group = displayio.Group(scale=2, x=184, y=83+60*1)
    fourth_text = "Plants"
    fourth_text_area = label.Label(terminalio.FONT, text=fourth_text, color=0x000000)
    fourth_text_group.append(fourth_text_area) # Subgroup for text scaling
    home_station_group.append(fourth_text_group)

    fifth_text_group = displayio.Group(scale=2, x=30, y=83+60*2)
    fifth_text = "Here/Now"
    fifth_text_area = label.Label(terminalio.FONT, text=fifth_text, color=0x000000)
    fifth_text_group.append(fifth_text_area) # Subgroup for text scaling
    home_station_group.append(fifth_text_group)

    sixth_text_group = displayio.Group(scale=2, x=184, y=83+60*2)
    sixth_text = "Sensors"
    sixth_text_area = label.Label(terminalio.FONT, text=sixth_text, color=0x000000)
    sixth_text_group.append(sixth_text_area) # Subgroup for text scaling
    home_station_group.append(sixth_text_group)

    ### above: create the main menu screen ###

    ### create the station groups ###
    print( "create the station display groups" )

    station_border_width = 6
    line_y = []
    first_line_y = 74
    line_spacing_y = 28
    small_text_offset_y = 4
    for n in range (0,6):
        line_y.append(74+n*line_spacing_y)

    light_station_group = displayio.Group()
    main_display_group.append( light_station_group )
    light_station_border = vectorio.Rectangle(pixel_shader=pal, color_index=10, width=320, height=184, x=0, y=56)
    light_station_group.append( light_station_border )
    light_station_background = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=320-2*station_border_width, height=184-2*station_border_width, x=0+station_border_width, y=56+station_border_width)
    light_station_group.append( light_station_background )
    light_station_group.hidden = True
    #
    light_station_tab = vectorio.Rectangle(pixel_shader=pal, color_index=10, width=60, height=16, x=0, y=40)
    light_station_group.append( light_station_tab )
    light_station_text_group = displayio.Group(scale=1, x=14, y=50)
    light_station_text = "Light:"
    light_station_text_area = label.Label(terminalio.FONT, text=light_station_text, color=0x000000)
    light_station_text_group.append(light_station_text_area) # Subgroup for text scaling
    light_station_group.append(light_station_text_group)
    #
    irrad_text = "irradiance"
    vert_spacing = 11
    for n in range( 0, len(irrad_text)):
        irrad_text_group = displayio.Group(scale=1, x=12, y=78+vert_spacing*n)
        irrad_letter = irrad_text[n]
        irrad_text_area = label.Label(terminalio.FONT, text=irrad_letter, color=0x000000 )
        irrad_text_group.append(irrad_text_area) # Subgroup for text scaling
        light_station_group.append(irrad_text_group)
    #
    light_linear_text_group = displayio.Group(scale=1, x=10, y=line_y[2])
    light_linear_text = "linear"
    light_linear_text_area = label.Label(terminalio.FONT, text=light_linear_text, color=0x000000)
    light_linear_text_group.append(light_linear_text_area) # Subgroup for text scaling
    #light_station_group.append(light_linear_text_group)
    #
    light_graph_x = 24
    light_graph_y = 70
    light_graph_width = 280
    light_graph_height = 116
    light_graph_border_width = 2
    light_graph_border = vectorio.Rectangle(pixel_shader=pal, color_index=0, width=light_graph_width, height=light_graph_height, x=light_graph_x, y=light_graph_y)
    light_station_group.append( light_graph_border )
    light_graph_background = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=light_graph_width-2*light_graph_border_width, height=light_graph_height-2*light_graph_border_width, x=light_graph_x+light_graph_border_width, y=light_graph_y+light_graph_border_width)
    light_station_group.append( light_graph_background )

    wavelength_y = 194
    light_wavelength_text_group = displayio.Group(scale=1, x=120, y=wavelength_y)
    light_wavelength_text = "wavelength nm"
    light_wavelength_text_area = label.Label(terminalio.FONT, text=light_wavelength_text, color=0x000000)
    light_wavelength_text_group.append(light_wavelength_text_area) # Subgroup for text scaling
    light_station_group.append(light_wavelength_text_group)

    light_wavelength_shortest_text_group = displayio.Group(scale=1, x=20, y=wavelength_y)
    light_wavelength_shortest_text = "000"
    light_wavelength_shortest_text_area = label.Label(terminalio.FONT, text=light_wavelength_shortest_text, color=0x000000)
    light_wavelength_shortest_text_group.append(light_wavelength_shortest_text_area) # Subgroup for text scaling
    light_station_group.append(light_wavelength_shortest_text_group)

    light_wavelength_longest_text_group = displayio.Group(scale=1, x=290, y=wavelength_y)
    light_wavelength_longest_text = "000"
    light_wavelength_longest_text_area = label.Label(terminalio.FONT, text=light_wavelength_longest_text, color=0x000000)
    light_wavelength_longest_text_group.append(light_wavelength_longest_text_area) # Subgroup for text scaling
    light_station_group.append(light_wavelength_longest_text_group)








    heat_station_group = displayio.Group()
    main_display_group.append( heat_station_group )
    heat_station_border = vectorio.Rectangle(pixel_shader=pal, color_index=11, width=320, height=184, x=0, y=56)
    heat_station_group.append( heat_station_border )
    heat_station_background = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=320-2*station_border_width, height=184-2*station_border_width, x=0+station_border_width, y=56+station_border_width)
    heat_station_group.append( heat_station_background )
    heat_station_group.hidden = True
    heat_station_text_group = displayio.Group(scale=2, x=10, y= 90)
    heat_station_text = "Heat: readings from\nambient and remote\nthermal IR thermometers\nand surface-air temp diff"
    heat_station_text_area = label.Label(terminalio.FONT, text=heat_station_text, color=0x000000)
    heat_station_text_group.append(heat_station_text_area) # Subgroup for text scaling
    heat_station_group.append(heat_station_text_group)

    air_station_group = displayio.Group()
    main_display_group.append( air_station_group )
    air_station_border = vectorio.Rectangle(pixel_shader=pal, color_index=12, width=320, height=184, x=0, y=56)
    air_station_group.append( air_station_border )
    air_station_background = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=320-2*station_border_width, height=184-2*station_border_width, x=0+station_border_width, y=56+station_border_width)
    air_station_group.append( air_station_background )
    air_station_group.hidden = True
    #
    air_station_text_group = displayio.Group(scale=2, x=10, y=line_y[0])
    air_station_text = "Air"
    air_station_text_area = label.Label(terminalio.FONT, text=air_station_text, color=0x000000)
    air_station_text_group.append(air_station_text_area) # Subgroup for text scaling
    air_station_group.append(air_station_text_group)
    #
    air_station_temperature_text_group = displayio.Group(scale=1, x=48, y=line_y[0]+small_text_offset_y)
    air_station_temperature_text = "Temperature:"
    air_station_temperature_text_area = label.Label(terminalio.FONT, text=air_station_temperature_text, color=0x000000)
    air_station_temperature_text_group.append(air_station_temperature_text_area) # Subgroup for text scaling
    air_station_group.append(air_station_temperature_text_group)
    #
    air_temp_text_group = displayio.Group(scale=2, x=124, y=line_y[0])
    air_temp_text = "-- C"
    air_temp_text_area = label.Label(terminalio.FONT, text=air_temp_text, color=0x000000)
    air_temp_text_group.append(air_temp_text_area) # Subgroup for text scaling
    air_station_group.append(air_temp_text_group)
    #
    air_equals_text_group = displayio.Group(scale=2, x=240, y=line_y[0])
    air_equals_text = "="
    air_equals_text_area = label.Label(terminalio.FONT, text=air_equals_text, color=0x000000)
    air_equals_text_group.append(air_equals_text_area) # Subgroup for text scaling
    air_station_group.append(air_equals_text_group)
    #
    air_tempF_text_group = displayio.Group(scale=2, x=258, y=line_y[0])
    air_tempF_text = "-- F"
    air_tempF_text_area = label.Label(terminalio.FONT, text=air_tempF_text, color=0x000000)
    air_tempF_text_group.append(air_tempF_text_area) # Subgroup for text scaling
    air_station_group.append(air_tempF_text_group)
    #
    air_temp_uncty_text_group = displayio.Group(scale=1, x=198, y=line_y[0]+small_text_offset_y)
    air_temp_uncty_text = " --"
    air_temp_uncty_text_area = label.Label(terminalio.FONT, text=air_temp_uncty_text, color=0x000000)
    air_temp_uncty_text_group.append(air_temp_uncty_text_area) # Subgroup for text scaling
    air_station_group.append(air_temp_uncty_text_group)
    #
    air_pressure_title_text_group = displayio.Group(scale=1, x=10, y=line_y[1]+small_text_offset_y)
    air_pressure_title_text = "Pressure:"
    air_pressure_title_text_area = label.Label(terminalio.FONT, text=air_pressure_title_text, color=0x000000)
    air_pressure_title_text_group.append(air_pressure_title_text_area) # Subgroup for text scaling
    air_station_group.append(air_pressure_title_text_group)
    #
    air_press_text_group = displayio.Group(scale=2, x=70, y=line_y[1])
    air_press_text = "---- hPa"
    air_press_text_area = label.Label(terminalio.FONT, text=air_press_text, color=0x000000)
    air_press_text_group.append(air_press_text_area) # Subgroup for text scaling
    air_station_group.append(air_press_text_group)
    #
    air_altitude_title_text_group = displayio.Group(scale=1, x=175, y=line_y[1]+small_text_offset_y)
    air_altitude_title_text = "Altitude:"
    air_altitude_title_text_area = label.Label(terminalio.FONT, text=air_altitude_title_text, color=0x000000)
    air_altitude_title_text_group.append(air_altitude_title_text_area) # Subgroup for text scaling
    air_station_group.append(air_altitude_title_text_group)
    #
    air_alt_text_group = displayio.Group(scale=2, x=235, y=line_y[1])
    air_alt_text = "-- m"
    air_alt_text_area = label.Label(terminalio.FONT, text=air_alt_text, color=0x000000)
    air_alt_text_group.append(air_alt_text_area) # Subgroup for text scaling
    air_station_group.append(air_alt_text_group)
    #
    air_humidity_title_text_group = displayio.Group(scale=1, x=10, y=line_y[2]+small_text_offset_y)
    air_humidity_title_text = "Humidity:"
    air_humidity_title_text_area = label.Label(terminalio.FONT, text=air_humidity_title_text, color=0x000000)
    air_humidity_title_text_group.append(air_humidity_title_text_area) # Subgroup for text scaling
    air_station_group.append(air_humidity_title_text_group)
    #
    air_humid_text_group = displayio.Group(scale=2, x=70, y=line_y[2])
    air_humid_text = "-- %"
    air_humid_text_area = label.Label(terminalio.FONT, text=air_humid_text, color=0x000000)
    air_humid_text_group.append(air_humid_text_area) # Subgroup for text scaling
    air_station_group.append(air_humid_text_group)
    #
    air_dewpoint_title_text_group = displayio.Group(scale=1, x=144, y=line_y[2]+small_text_offset_y)
    air_dewpoint_title_text = "Dewpoint:"
    air_dewpoint_title_text_area = label.Label(terminalio.FONT, text=air_dewpoint_title_text, color=0x000000)
    air_dewpoint_title_text_group.append(air_dewpoint_title_text_area) # Subgroup for text scaling
    air_station_group.append(air_dewpoint_title_text_group)
    #
    air_dewpoint_text_group = displayio.Group(scale=2, x=204, y=line_y[2])
    air_dewpoint_text = "-- C"
    air_dewpoint_text_area = label.Label(terminalio.FONT, text=air_dewpoint_text, color=0x000000)
    air_dewpoint_text_group.append(air_dewpoint_text_area) # Subgroup for text scaling
    air_station_group.append(air_dewpoint_text_group)
    #
    air_particulates_title_text_group = displayio.Group(scale=1, x=10, y=line_y[4]+small_text_offset_y)
    air_particulates_title_text = "PM2.5:"
    air_particulates_title_text_area = label.Label(terminalio.FONT, text=air_particulates_title_text, color=0x000000)
    air_particulates_title_text_group.append(air_particulates_title_text_area) # Subgroup for text scaling
    air_station_group.append(air_particulates_title_text_group)
    #
    air_particulates_title2_text_group = displayio.Group(scale=1, x=120, y=line_y[4]+small_text_offset_y)
    air_particulates_title2_text = "PM10:"
    air_particulates_title2_text_area = label.Label(terminalio.FONT, text=air_particulates_title2_text, color=0x000000)
    air_particulates_title2_text_group.append(air_particulates_title2_text_area) # Subgroup for text scaling
    air_station_group.append(air_particulates_title2_text_group)
    #
    air_particulates_title2_text_group = displayio.Group(scale=1, x=10, y=line_y[5]+small_text_offset_y)
    air_particulates_title2_text = "PM2.5/PM10:"
    air_particulates_title2_text_area = label.Label(terminalio.FONT, text=air_particulates_title2_text, color=0x000000)
    air_particulates_title2_text_group.append(air_particulates_title2_text_area) # Subgroup for text scaling
    air_station_group.append(air_particulates_title2_text_group)
    #
    air_pm25_text_group = displayio.Group(scale=2, x=55, y=line_y[4])
    air_pm25_text = "--"
    air_pm25_text_area = label.Label(terminalio.FONT, text=air_pm25_text, color=0x000000)
    air_pm25_text_group.append(air_pm25_text_area) # Subgroup for text scaling
    air_station_group.append(air_pm25_text_group)
    #
    air_pm100_text_group = displayio.Group(scale=2, x=160, y=line_y[4])
    air_pm100_text = "--"
    air_pm100_text_area = label.Label(terminalio.FONT, text=air_pm100_text, color=0x000000)
    air_pm100_text_group.append(air_pm100_text_area) # Subgroup for text scaling
    air_station_group.append(air_pm100_text_group)
    #
    air_pm_units_text_group = displayio.Group(scale=2, x=250, y=line_y[4])
    air_pm_units_text = "ug/m"
    air_pm_units_text_area = label.Label(terminalio.FONT, text=air_pm_units_text, color=0x000000)
    air_pm_units_text_group.append(air_pm_units_text_area) # Subgroup for text scaling
    air_station_group.append(air_pm_units_text_group)
    #
    air_pm_superscript_text_group = displayio.Group(scale=1, x=300, y=line_y[4]-small_text_offset_y)
    air_pm_superscript_text = "3" #superscript 3
    air_pm_superscript_text_area = label.Label(terminalio.FONT, text=air_pm_superscript_text, color=0x000000)
    air_pm_superscript_text_group.append(air_pm_superscript_text_area) # Subgroup for text scaling
    air_station_group.append(air_pm_superscript_text_group)
    #
    air_pm_ratio_text_group = displayio.Group(scale=2, x=84, y=line_y[5])
    air_pm_ratio_text = "--"
    air_pm_ratio_text_area = label.Label(terminalio.FONT, text=air_pm_ratio_text, color=0x000000)
    air_pm_ratio_text_group.append(air_pm_ratio_text_area) # Subgroup for text scaling
    air_station_group.append(air_pm_ratio_text_group)
    #
    air_aqip_title_text_group = displayio.Group(scale=1, x=160, y=line_y[5]+small_text_offset_y)
    air_aqip_title_text = "AQIp:"
    air_aqip_title_text_area = label.Label(terminalio.FONT, text=air_aqip_title_text, color=0x000000)
    air_aqip_title_text_group.append(air_aqip_title_text_area) # Subgroup for text scaling
    air_station_group.append(air_aqip_title_text_group)
    #
    air_aqip_text_group = displayio.Group(scale=2, x=197, y=line_y[5])
    air_aqip_text = "--"
    air_aqip_text_area = label.Label(terminalio.FONT, text=air_aqip_text, color=0x000000)
    air_aqip_text_group.append(air_aqip_text_area) # Subgroup for text scaling
    air_station_group.append(air_aqip_text_group)
    #
    air_co2_title_text_group = displayio.Group(scale=1, x=10, y=line_y[3]+small_text_offset_y)
    air_co2_title_text = "CO :"
    air_co2_title_text_area = label.Label(terminalio.FONT, text=air_co2_title_text, color=0x000000)
    air_co2_title_text_group.append(air_co2_title_text_area) # Subgroup for text scaling
    air_station_group.append(air_co2_title_text_group)
    #
    air_co2_sub_text_group = displayio.Group(scale=1, x=22, y=line_y[3]+2*small_text_offset_y)
    air_co2_sub_text = "2"
    air_co2_sub_text_area = label.Label(terminalio.FONT, text=air_co2_sub_text, color=0x000000)
    air_co2_sub_text_group.append(air_co2_sub_text_area) # Subgroup for text scaling
    air_station_group.append(air_co2_sub_text_group)
    #
    air_co2_text_group = displayio.Group(scale=2, x=40, y=line_y[3])
    air_co2_text = "---"
    air_co2_text_area = label.Label(terminalio.FONT, text=air_co2_text, color=0x000000)
    air_co2_text_group.append(air_co2_text_area) # Subgroup for text scaling
    air_station_group.append(air_co2_text_group)
    #
    air_ch4_title_text_group = displayio.Group(scale=1, x=100, y=line_y[3]+small_text_offset_y)
    air_ch4_title_text = "CH :"
    air_ch4_title_text_area = label.Label(terminalio.FONT, text=air_ch4_title_text, color=0x000000)
    air_ch4_title_text_group.append(air_ch4_title_text_area) # Subgroup for text scaling
    air_station_group.append(air_ch4_title_text_group)
    #
    air_ch4_sub_text_group = displayio.Group(scale=1, x=112, y=line_y[3]+2*small_text_offset_y)
    air_ch4_sub_text = "4"
    air_ch4_sub_text_area = label.Label(terminalio.FONT, text=air_ch4_sub_text, color=0x000000)
    air_ch4_sub_text_group.append(air_ch4_sub_text_area) # Subgroup for text scaling
    air_station_group.append(air_ch4_sub_text_group)
    #
    air_ch4_text_group = displayio.Group(scale=2, x=130, y=line_y[3])
    air_ch4_text = "--"
    air_ch4_text_area = label.Label(terminalio.FONT, text=air_ch4_text, color=0x000000)
    air_ch4_text_group.append(air_ch4_text_area) # Subgroup for text scaling
    air_station_group.append(air_ch4_text_group)
    #
    air_ppm_text_group = displayio.Group(scale=2, x=210, y=line_y[3])
    air_ppm_text = "ppm"
    air_ppm_text_area = label.Label(terminalio.FONT, text=air_ppm_text, color=0x000000)
    air_ppm_text_group.append(air_ppm_text_area) # Subgroup for text scaling
    air_station_group.append(air_ppm_text_group)
    #
    plants_station_group = displayio.Group()
    main_display_group.append( plants_station_group )
    plants_station_border = vectorio.Rectangle(pixel_shader=pal, color_index=13, width=320, height=184, x=0, y=56)
    plants_station_group.append( plants_station_border )
    plants_station_background = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=320-2*station_border_width, height=184-2*station_border_width, x=0+station_border_width, y=56+border_width)
    plants_station_group.append( plants_station_background )
    plants_station_group.hidden = True
    plants_station_text_group = displayio.Group(scale=2, x=10, y= 90)
    plants_station_text = "Plants: dT for \nevapotranspiration\nspectral plot\nveg indicies"
    plants_station_text_area = label.Label(terminalio.FONT, text=plants_station_text, color=0x000000)
    plants_station_text_group.append(plants_station_text_area) # Subgroup for text scaling
    plants_station_group.append(plants_station_text_group)

    herenow_station_group = displayio.Group()
    main_display_group.append( herenow_station_group )
    herenow_station_border = vectorio.Rectangle(pixel_shader=pal, color_index=14, width=320, height=184, x=0, y=56)
    herenow_station_group.append( herenow_station_border )
    herenow_station_background = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=320-2*station_border_width, height=184-2*station_border_width, x=0+station_border_width, y=56+border_width)
    herenow_station_group.append( herenow_station_background )
    herenow_station_group.hidden = True
    herenow_station_text_group = displayio.Group(scale=2, x=10, y= 90)
    herenow_station_text = "Here/Now: GPS, clock\nset clock\nsync clock to GPS time"
    herenow_station_text_area = label.Label(terminalio.FONT, text=herenow_station_text, color=0x000000)
    herenow_station_text_group.append(herenow_station_text_area) # Subgroup for text scaling
    herenow_station_group.append(herenow_station_text_group)

    sensors_station_group = displayio.Group()
    main_display_group.append( sensors_station_group )
    sensors_station_border = vectorio.Rectangle(pixel_shader=pal, color_index=15, width=320, height=184, x=0, y=56)
    sensors_station_group.append( sensors_station_border )
    sensors_station_background = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=320-2*station_border_width, height=184-2*station_border_width, x=0+station_border_width, y=56+border_width)
    sensors_station_group.append( sensors_station_background )
    sensors_station_group.hidden = True
    sensors_station_text_group = displayio.Group(scale=2, x=10, y= 90)
    sensors_station_text = "Sensors: list of\nactive sensors and\nsupported but not found"
    sensors_station_text_area = label.Label(terminalio.FONT, text=sensors_station_text, color=0x000000)
    sensors_station_text_group.append(sensors_station_text_area) # Subgroup for text scaling
    sensors_station_group.append(sensors_station_text_group)

    #make a lower right button
    lr_button_group = displayio.Group()
    lr_button_group.hidden = True
    lr_button_width = 60
    lr_button_height = 34
    main_display_group.append( lr_button_group )
    lr_button_background = vectorio.Rectangle(pixel_shader=pal, color_index=0,
                        width=lr_button_width, height=lr_button_height, x=320-lr_button_width-4, y=240-lr_button_height-4)
    lr_button_group.append( lr_button_background )
    lr_button_background = vectorio.Rectangle(pixel_shader=pal, color_index=9, width=lr_button_width-4, height=lr_button_height-4, x=320-lr_button_width-4+2, y=240-lr_button_height-4+2)
    lr_button_group.append( lr_button_background )
    lr_button_text_group = displayio.Group(scale=2, x=320-lr_button_width+4, y=240-int(lr_button_height/2)-4)
    lr_button_text = "Home"
    lr_button_text_area = label.Label(terminalio.FONT, text=lr_button_text, color=0x000000)
    lr_button_text_group.append(lr_button_text_area) # Subgroup for text scaling
    lr_button_group.append(lr_button_text_group)


    memory_check( "after main menu screen assignments" )
    jump = 0
    operational = True

    if spectrometry:
        if vis_nir_triad_spectrometer.swob:
            print("triad graph mode")
            ## set spectral defaults ##
            triad_gain_number = 3 # default is 3 = 64x gain
            triad_last_gain_number = triad_gain_number
            vis_nir_triad_spectrometer.set_gain_number(triad_gain_number)
            print( "triad spectrometer gain set to {} X".format( vis_nir_triad_spectrometer.check_gain_ratio() ))

            ## set triad integration time and report setting ##
            triad_intg_cycles = 57 #0-255, 0-717ms # default is 57 = TBD ms.
            triad_last_intg_cycles = triad_intg_cycles
            triad_intg_time_ms = vis_nir_triad_spectrometer.set_integration_cycles(triad_intg_cycles)
            print("triad spectrometer integration time set to {} ms".format(triad_intg_time_ms))
        if uv_spectrometer.swob:
            print("uv graph mode")
            uv_gain_number = 5 # dafault is TBD
            uv_last_gain_number = uv_gain_number
            uv_spectrometer.set_gain_number(uv_gain_number)
            print( "uv spectrometer gain set to {} X".format( uv_spectrometer.check_gain_ratio() ))

            ## set uv integration time and report setting ##
            uv_intg_number = 6 # number 6, integration time of 64ms is the factory cal setting
            uv_last_intg_number = uv_intg_number
            uv_intg_time_ms = uv_spectrometer.set_integration_time(uv_intg_number)
            print("uv spectrometer integration time set to {} ms".format(uv_intg_time_ms))
        if vis_nir_triad_spectrometer.swob and uv_spectrometer.swob:
            print("override: uv and triad graph mode")


        timenow = read_time( hardware_clock )
        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 )

        #read the spectrometers
        uv_spectrometer.read()
        vis_nir_triad_spectrometer.read()
            #get raw counts, f-cal irrad, calculate f-cal uncty, gain, intg, bw, fov, chiptemp

        # build a wavelength channel list from all sensors present
        triad_gain = vis_nir_triad_spectrometer.check_gain_ratio()
        uv_gain = uv_spectrometer.check_gain_ratio()
        if uv_spectrometer.swob:
            wl_channel_list = uv_spectrometer.bands
        elif vis_nir_triad_spectrometer.swob:
            wl_channel_list = vis_nir_triad_spectrometer.bands
        if uv_spectrometer.swob and vis_nir_triad_spectrometer.swob:
            wl_channel_list += vis_nir_triad_spectrometer.bands
        wl_channel_list_sorted = sorted( wl_channel_list )
        if False:
            for channel in wl_channel_list_sorted:
                #print( iso8601_utc, decimal_hour, UID, batch, burst_mmt, end = " ")
                if channel in uv_spectrometer.dict_chip_n:
                    print( uv_spectrometer.dict_chip_n[channel], end = " ")
                    print( channel, uv_spectrometer.dict_counts[channel], end = ", " )
                    print( 0, 0, end = " ") # s_cal and s_cal_uncty
                    u = uv_spectrometer.dict_fcal[channel]
                    u_uncty = u * uv_spectrometer.fcal_unct_percent/100
                    print( u, u_uncty, end = " " )
                    print( uv_gain,  end = " " )
                    print( uv_intg_time_ms, end = " " )
                    print( uv_spectrometer.dict_bw_fwhm_nm[channel], end = " " )
                    print( uv_spectrometer.afov_deg, end = " " )
                    print( uv_spectrometer.chip_temp_c)

                if channel in vis_nir_triad_spectrometer.dict_chip_n:
                    chip_number = vis_nir_triad_spectrometer.dict_chip_n[channel]
                    print( "as7265{}".format( chip_number ), end = " ")
                    print( channel, vis_nir_triad_spectrometer.dict_counts[channel], end = " ")
                    print( 0, 0, end = " ") # s_cal and s_cal_uncty
                    a = vis_nir_triad_spectrometer.dict_fcal[channel]
                    a_uncty = a * vis_nir_triad_spectrometer.uncertainty_percent / 100
                    print( a, a_uncty, end = " " )
                    print( triad_gain,  end = " " )
                    print( triad_intg_time_ms, end = " " )
                    print( vis_nir_triad_spectrometer.bw_fwhm_nm, end = " " )
                    print( vis_nir_triad_spectrometer.afov_deg, end = " " )
                    print( vis_nir_triad_spectrometer.chip_temp_c[chip_number])


    gc.collect()
    loop_count = 0
    interval_start_time_s = time.monotonic()
    print("operational")
    recording = True
    last_touched = False
    if recording:
        print( "recording" )
    selector_choice = 0 #park
    selector_number_of_choices = 9
    last_pressed = False
    pressed = False
    station = "home"
    if co2_photoacoustic.swob and particulates_counter.swob:
        station = "air"
    if vis_nir_triad_spectrometer.swob:
        station = "light"


    while operational:
        if recording:
            record_circle.hidden = False
        else:
            record_circle.hidden = True

        batch_number_text = "{}".format( batch_number )
        #if batch_number < 100:
        #    batch_number_text = " " + batch_number_text
        #if batch_number < 10:
        #    batch_number_text = " " + batch_number_text
        bt_text_area.text = batch_number_text
        #batch_number += 1
        #if batch_number > 120:
        #    batch_number = 0
        time.sleep(.2)

        # TBD problem is batch_number contains a properly formatted string
        # I think it's misleading to call something a number and have it contain a string.






        gc.collect()
        loop_count += 1
        if loop_count > 65535:
            loop_count = 0
        gps.read()
        battery_monitor.read()

        ### begin graphics interface handling ###
        if True: # update battery monitor icon
            a = battery_monitor.percentage
            if a > 95:
                a = 100
            bat_show_segments = int(a/100*(number_of_bat_segments))
            for n in range (0, bat_show_segments):
                bat_segment[n].hidden = False
            for n in range( bat_show_segments, number_of_bat_segments):
                bat_segment[n].hidden = True
        if True: #update GPS flag
            if gps.swob.has_fix:
                gps_fix_flag_rect.hidden = False
                gps_fix_text_area.color = 0xFFFFFF
                gps_fix_text_area.text = "GPS:FIX"
            else:
                gps_fix_flag_rect.hidden = True
                gps_fix_text_area.color = 0x000000
                gps_fix_text_area.text = "GPS:nofix"


        encoder_position = encoder.position
        if last_encoder_position is not None and encoder_position != last_encoder_position:
            jump = encoder_position - last_encoder_position
        last_encoder_position = encoder_position
        #mm_choice = (mm_choice + jump) % mm_number_of_choices
        selector_choice = (selector_choice + jump) % selector_number_of_choices
        #print( selector_choice )
        for n in range (0, len(highlight)):
            highlight[n].hidden = True
        highlight[selector_choice].hidden = False
        jump = 0
        if not encoder_button.value:
            button_pushed = True
            if not last_pressed:
                #print(" do something ")
                last_pressed = True
                if selector_choice == 0:
                    pass
                if selector_choice == 1:
                    recording = not recording
                if selector_choice == 2:
                    print("burst")
                if station == "home":
                    if selector_choice == 3:
                        station = "light"
                    if selector_choice == 4:
                        station = "heat"
                    if selector_choice == 5:
                        station = "air"
                    if selector_choice == 6:
                        station = "plants"
                    if selector_choice == 7:
                        station = "herenow"
                    if selector_choice == 8:
                        station = "sensors"
        else:
            button_pushed = False
            last_pressed = False

        touch_screen.read()

        if touch_screen.is_touched:
            screen_touched = True
            print("touched", touch_screen.tx, touch_screen.ty)
            if not last_touched:
                if touch_screen.ty < 60:
                    if touch_screen.tx in range (114, 183) :
                        recording = not recording
                        # record/pause
                    if touch_screen.tx in range (184, 235):
                        print( "burst" )
                if station == "home":
                    if touch_screen.ty > 60:
                        if touch_screen.ty < 120:
                            if touch_screen.tx <160:
                                station = "light"
                            else:
                                station = "heat"
                        elif touch_screen.ty < 180:
                            if touch_screen.tx <160:
                                station = "air"
                            else:
                                station = "plants"
                        else: #y in range (180, 240)
                            if touch_screen.tx <160:
                                station = "herenow"
                            else:
                                station = "sensors"
                else:
                    #x>280, y>210
                    if touch_screen.ty > 210 and touch_screen.tx >280:
                        station = "home"
            last_touched = True
        else:
            screen_touched = False
            last_touched = False

        if screen_touched or button_pushed:
            background.hidden = True
            print( "station == {}".format(station))
        else:
            background.hidden = False

        if station == "home":
            home_station_group.hidden = False
            lr_button_group.hidden = True
        else:
            home_station_group.hidden = True
            lr_button_group.hidden = False

        if station == "light":
            light_station_group.hidden = False
        else:
            light_station_group.hidden = True

        if station == "heat":
            heat_station_group.hidden = False
        else:
            heat_station_group.hidden = True

        if station == "air":
            air_station_group.hidden = False
        else:
            air_station_group.hidden = True

        if station == "plants":
            plants_station_group.hidden = False
        else:
            plants_station_group.hidden = True

        if station == "herenow":
            herenow_station_group.hidden = False
        else:
            herenow_station_group.hidden = True

        if station == "sensors":
            sensors_station_group.hidden = False
        else:
            sensors_station_group.hidden = True


        ### end graphics interface handling ####


        # 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:
            interval_start_time_s = time.monotonic()

            # begin burst control
            burst_mmt = 0
            while burst_mmt < burst_count:
                burst_mmt += 1
                #print( "burst_mmt", burst_mmt )
                # read all the point sensors
                if lidar_4m_range:
                    lidar_range_m = lidar_4m_range.distance/100
                else:
                    lidar_range_m = 0
                tir_surface_thermometer.read()
                high_accuracy_air_thermometer.read()
                ambient_air_sensor.read()
                co2_photoacoustic.read()
                if ch4_chemoresistor:
                    supply_5V_voltage = get_analog_voltage( monitor_5V )
                    #print( "supply voltage", supply_5V_voltage )
                    ch4_chemoresistor_voltage = get_analog_voltage( ch4_signal )
                    #print( "ch4 signal voltage", ch4_chemoresistor_voltage )
                    #print( "spare analog signal", get_analog_voltage( spare_analog ) )
                    ch4_chemoresistor_ppm = 10.938 * math.exp(1.7742*ch4_chemoresistor_voltage)
                    #print( "ch4 signal ppm", ch4_chemoresistor_ppm )
                    # Methane sensor: PPM = 10.938*e(1.7742*V_RL)
                    # from https://learn.sparkfun.com/tutorials/hazardous-gas-monitor
                particulates_counter.read()
                #print( particulates_counter.log() )


                ## point data order ##
                # iso8601_utc, decimal_hour, UID, batch, burst_mmt
                # lat.deg.N, long.deg.W, alt.m.gps, iso8601_gps
                # range.m
                # surf.temp.C
                # ha.air.temp.C
                # amb.air.temp.C, amb.air.humid.percent, amb.air.dewpt.C, amb.air.press.hPa, amb.press_alt.m
                # co2.photoacoustic.ppm, co2.photoacoustic.uncty.ppm, co2.sensor.temp.C, co2.sensor.humid.percent
                # ch4.raw.counts, ch4.ppm, 5V.supply.V
                # bat.V, bat.percent
                # particulates.aqi_p, particulates.1um_ug.per.m.cu, particulates.2.5um_ug.per.m.cu, particulates.10um_ug.per.m.cu
                #   particulates.0.3um_ct.per.100mL, particulates.0.5um_ct.per.100mL
                #   particulates.1um_ct.per.100mL, particulates.2.5um_ct.per.100mL
                #   particulates.10um_ct.per.100mL, particulates.100um_ct.per.100mL

                timenow = read_time( hardware_clock )
                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 )

                if recording:
                    try:
                        with open( "/sd/{}".format( point_filename_in_use ), "a" ) as f:
                            if onboard_neopixel:
                                onboard_neopixel.fill ( (255,0,0) )
                            f.write( "{}, {}, {}, {}, {}".format( iso8601_utc, decimal_hour, UID, batch_number, burst_mmt ))
                            if gps.swob:
                                f.write( ", " )
                                f.write( gps.log())
                            if lidar_4m_range:
                                f.write( ", " )
                                f.write( "{}".format( lidar_range_m ))
                            if tir_surface_thermometer.swob:
                                f.write( ", " )
                                f.write( tir_surface_thermometer.log() )
                            if high_accuracy_air_thermometer.swob:
                                f.write( ", " )
                                f.write( high_accuracy_air_thermometer.log() )
                            if ambient_air_sensor.swob:
                                f.write( ", " )
                                f.write( ambient_air_sensor.log())
                            if co2_photoacoustic.swob:
                                f.write( ", " )
                                f.write( co2_photoacoustic.log() )
                                if ch4_if_co2:
                                    f.write( ", " )
                                    f.write( "{}, {}, {}".format( ch4_chemoresistor_ppm, ch4_chemoresistor_voltage, supply_5V_voltage ))
                            if battery_monitor.swob:
                                f.write( ", " )
                                f.write( battery_monitor.log() )
                            if particulates_counter.swob:
                                f.write( ", " )
                                f.write( particulates_counter.log())
                            f.write("\n")
                            time.sleep( 0.005 )
                            f.close()
                        if onboard_neopixel:
                            onboard_neopixel.fill( (0,0,0))
                    except OSError as err:
                        print( "\nError: sd card fail: {:}\n".format(err) )
                        if onboard_neopixel:
                            onboard_neopixel.fill( (102, 255, 0) ) #  fault_color to show error: likely no SD card present, or SD card full.
                            time.sleep( 1 )
                            onboard_neopixel.fill( (0,0,0) )
                        recording = False

                if spectrometry:
                    #print( "spectrometry" )

                    if vis_nir_triad_spectrometer.swob:
                        # read a new triad gain number from user interface
                        if triad_gain_number != triad_last_gain_number:
                            triad_last_gain_number = triad_gain_number
                            vis_nir_triad_spectrometer.set_gain_number(triad_gain_number)
                            print( "triad spectrometer gain set to {} X".format( vis_nir_triad_spectrometer.check_gain_ratio() ))

                        # read a new triad integration cycles from user interface
                        if triad_intg_cycles != triad_last_intg_cycles:
                            triad_last_intg_cycles = triad_intg_cycles
                            triad_intg_time_ms = vis_nir_triad_spectrometer.set_integration_cycles(triad_intg_cycles)
                            print("triad spectrometer integration time set to {} ms".format(triad_intg_time_ms))

                        triad_gain = vis_nir_triad_spectrometer.check_gain_ratio()

                    if uv_spectrometer.swob:
                        # read a new uv gain number from user interface
                        if uv_gain_number != uv_last_gain_number:
                            uv_last_gain_number = uv_gain_number
                            uv_spectrometer.set_gain_number(uv_gain_number)
                            print( "uv spectrometer gain set to {} X".format( uv_spectrometer.check_gain_ratio() ))
                        uv_gain = uv_spectrometer.check_gain_ratio()

                    #read the spectrometers
                    uv_spectrometer.read()
                    vis_nir_triad_spectrometer.read()
                    if recording:
                        try:
                            with open( "/sd/{}".format( spectral_filename_in_use ), "a" ) as s:
                                if onboard_neopixel:
                                    onboard_neopixel.fill ( (255,0,255) )
                                s.write( "{},{},{},{},{},{},{},{}\n".format( iso8601_utc, decimal_hour, UID, batch_number, burst_mmt, 0, "line break", 0 ))
                                if lidar_4m_range:
                                    range_m = lidar_range_m
                                else:
                                    range_m = 0
                                for channel in wl_channel_list_sorted:
                                    s.write( "{}, {}, {}, {}, {}, {}".format( iso8601_utc, decimal_hour, UID, batch_number, burst_mmt, range_m ))

                                    if channel in uv_spectrometer.dict_chip_n:
                                        s.write( ", " )
                                        s.write( "{}".format(uv_spectrometer.dict_chip_n[channel] ))
                                        s.write( ", " )
                                        s.write( "{}".format( channel ))
                                        s.write( ", " )
                                        s.write( "{}".format(str(uv_spectrometer.dict_counts[channel] )))
                                        s.write( ", " )
                                        s.write( "{}, {}".format( 0, 0 ))# s_cal and s_cal_uncty
                                        u = uv_spectrometer.dict_fcal[channel]
                                        u_uncty = u * uv_spectrometer.fcal_unct_percent/100
                                        s.write( ", " )
                                        s.write( "{}".format( u ))
                                        s.write( ", " )
                                        s.write( "{}".format( u_uncty ))
                                        s.write( ", " )
                                        s.write( "{}".format( uv_gain ))
                                        s.write( ", " )
                                        s.write( "{}".format(uv_intg_time_ms ))
                                        s.write( ", " )
                                        s.write( "{}".format(uv_spectrometer.dict_bw_fwhm_nm[channel] ))
                                        s.write( ", " )
                                        s.write( "{}".format(uv_spectrometer.afov_deg ))
                                        s.write( ", " )
                                        s.write( "{}".format(uv_spectrometer.chip_temp_c ))

                                    if channel in vis_nir_triad_spectrometer.dict_chip_n:
                                        chip_number = vis_nir_triad_spectrometer.dict_chip_n[channel]
                                        s.write( ", " )
                                        s.write( "as7265{}".format( chip_number ))
                                        s.write( ", " )
                                        s.write( "{}".format( channel ))
                                        s.write( ", " )
                                        s.write( "{}".format( vis_nir_triad_spectrometer.dict_counts[channel]))
                                        s.write( ", " )
                                        s.write( "{}, {}".format( 0, 0 ))# s_cal and s_cal_uncty
                                        a = vis_nir_triad_spectrometer.dict_fcal[channel]
                                        a_uncty = a * vis_nir_triad_spectrometer.uncertainty_percent / 100
                                        s.write( ", " )
                                        s.write( "{}".format(a ))
                                        s.write( ", " )
                                        s.write( "{}".format(a_uncty ))
                                        s.write( ", " )
                                        s.write( "{}".format(triad_gain ))
                                        s.write( ", " )
                                        s.write( "{}".format(triad_intg_time_ms ))
                                        s.write( ", " )
                                        s.write( "{}".format(vis_nir_triad_spectrometer.bw_fwhm_nm ))
                                        s.write( ", " )
                                        s.write( "{}".format(vis_nir_triad_spectrometer.afov_deg ))
                                        s.write( ", " )
                                        s.write( "{}".format(vis_nir_triad_spectrometer.chip_temp_c[chip_number]))
                                    s.write("\n")
                                    time.sleep(0.005)
                            s.close()
                            if onboard_neopixel:
                                onboard_neopixel.fill( (0,0,0))
                        except OSError as err:
                            print( "\nError: sd card fail: {:}\n".format(err) )
                            if onboard_neopixel:
                                onboard_neopixel.fill( (102, 255, 0) ) #  fault_color to show error: likely no SD card present, or SD card full.
                                time.sleep( 1 )
                                onboard_neopixel.fill( (0,0,0) )
                            recording = False
        # show live values
        show_air_temp = "-- C"
        air_temp_uncty = "--"
        air_temp = -274

        if co2_photoacoustic.temperature_C is not None:
            air_temp = co2_photoacoustic.temperature_C
            air_temp_uncty = "+/-3"
        if ambient_air_sensor.temperature_C is not None:
            air_temp = ambient_air_sensor.temperature_C
            air_temp_uncty = "+/-1"
        if high_accuracy_air_thermometer.temperature_C is not None:
            air_temp = high_accuracy_air_thermometer.temperature_C
            air_temp_uncty = "+/-0.3"
        if air_temp > -274:
            air_temp_text_area.text = "{} C".format(round(air_temp,1))
            air_tempF_text_area.text = "{} F".format( round(9*air_temp/5 +32), 0)
        else:
            air_temp_text_area.text = "-- C"
            air_tempF_text_area.text = "-- F"
        air_temp_uncty_text_area.text = air_temp_uncty

        air_pressure = 0
        if ambient_air_sensor.pressure is not None:
           air_pressure = ambient_air_sensor.pressure
        if air_pressure > 0:
            air_press_text_area.text = "{} hPa".format(int(ambient_air_sensor.pressure))
        else:
            air_press_text_area.text = "--- hPa"

        if ambient_air_sensor.altitude is not None:
            if ambient_air_sensor.altitude < 100:
                air_alt_text_area.text = "{} m".format(round(ambient_air_sensor.altitude,1))
            else:
                air_alt_text_area.text = "{} m".format(int(ambient_air_sensor.altitude))

        air_humidity = 0
        if co2_photoacoustic.humidity is not None:
            air_humidity = int(co2_photoacoustic.humidity)
        if ambient_air_sensor.humidity is not None:
            air_humidity = int(ambient_air_sensor.humidity)
        if air_humidity > 0:
            air_humid_text_area.text = "{} %".format(air_humidity)
        else:
            air_humid_text_area.text = "-- %"

        if air_humidity != 0 and air_temp<100:
            T = air_temp
            RH = air_humidity
            air_dewpoint = 243.04*(math.log(RH/100)+((17.625*T)/(243.04+T)))/(17.625-math.log(RH/100)-((17.625*T)/(243.04+T)))
            # from https://bmcnoldy.earth.miami.edu/Humidity.html
            air_dewpoint = round( air_dewpoint, 1)
            air_dewpoint_text_area.text = "{} C".format(air_dewpoint)
        else:
            air_dewpoint_text_area.text  = "-- C"

        aqip = -1
        if particulates_counter.aqip is not None:
            aqip = particulates_counter.aqip
        if aqip > -1:
            air_aqip_text_area.text = str(aqip)
        else:
            air_aqip_text_area.text = "--"

        air_pm25 = -1
        air_pm100 = -1
        if particulates_counter.pm25 is not None:
            air_pm25 = particulates_counter.pm25
        if air_pm25 > -1:
            air_pm25_text_area.text = str(air_pm25)
        else:
            air_pm25_text_area.text = "--"
        if particulates_counter.pm100 is not None:
            air_pm100 = particulates_counter.pm100
        if air_pm100 > -1:
            air_pm100_text_area.text = str(air_pm100)
        else:
            air_pm100_text_area.text = "--"

        pm_ratio = -1
        if air_pm100 > 0 and air_pm25 > 0:
            pm_ratio = round(air_pm25/air_pm100,1)

        if pm_ratio > -1:
            air_pm_ratio_text_area.text = str(pm_ratio)
        else:
            air_pm_ratio_text_area.text = "--"

        co2 = -1
        if co2_photoacoustic.co2_ppm is not None:
            co2 = co2_photoacoustic.co2_ppm
        if co2 > -1:
            air_co2_text_area.text = "{}".format(co2)
        else:
            air_co2_text_area.text = "--"

        ch4 = -1
        if ch4_chemoresistor_ppm is not None:
            ch4 = ch4_chemoresistor_ppm
        if ch4 > -1:
            air_ch4_text_area.text = "{}".format(int( ch4 ) )
        else:
            air_ch4_text_area.text = "--"


        if battery_monitor.percentage < 25:
                    print ("low battery (<25%)")
                    for i in range (5):
                        onboard_red_led.value = True
                        time.sleep(0.1)
                        onboard_red_led.value = False
                        time.sleep(0.1)

    try:
        pass
    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" )



## parent class ##
class Device:
    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 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 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 create_welcome_screen( display ):
    try:
        bitmap = displayio.OnDiskBitmap("/lib/stella_logo.bmp")
        #print( "Bitmap image file found" )
        # Create a TileGrid to hold the bitmap
        tile_grid = displayio.TileGrid(bitmap, pixel_shader=bitmap.pixel_shader)
        # Create a Group to hold the TileGrid
        welcome_group = displayio.Group()
        # Add the TileGrid to the Group
        welcome_group.append(tile_grid)

        version_group = displayio.Group( scale=2, x=40, y=185 )
        text = "STELLA-1.2 ver {}".format( SOFTWARE_VERSION_NUMBER )
        version_area = label.Label( terminalio.FONT, text=text, color=0x000000 )
        version_group.append( version_area ) # Subgroup for text scaling
        welcome_group.append( version_group )

        #battery_group = displayio.Group( scale=2, x=90, y=215 )
        #text = "battery {}%".format( battery_level )
        #battery_area = label.Label( terminalio.FONT, text=text, color=0x000000 )
        #battery_group.append( battery_area ) # Subgroup for text scaling
        #welcome_group.append( battery_group )
        print( "showing welcome screen with logo")
    except (MemoryError, OSError):
        print( "bitmap image file not found or memory not available" )
        welcome_group = displayio.Group()
        border_color = 0xFF0022 # red
        front_color = 0x0000FF # blue
        if (display == False) or ( welcome_group == False):
            print("No display")
            return
        border = displayio.Palette(1)
        border[0] = border_color
        front = displayio.Palette(1)
        front[0] = front_color
        outer_rectangle = vectorio.Rectangle(pixel_shader=border, width=320, height=240, x=0, y=0)
        welcome_group.append( outer_rectangle )
        front_rectangle = vectorio.Rectangle(pixel_shader=front, width=280, height=200, x=20, y=20)
        welcome_group.append( front_rectangle )
        text_group = displayio.Group( scale=4, x=45, y=110 )
        text = "STELLA-1.2"
        text_area = label.Label( terminalio.FONT, text=text, color=0xFFFFFF )
        text_group.append( text_area ) # Subgroup for text scaling
        welcome_group.append( text_group )

        version_group = displayio.Group( scale=2, x=27, y=200 )
        text = "software version {}".format( SOFTWARE_VERSION_NUMBER )
        version_area = label.Label( terminalio.FONT, text=text, color=0xFFFFFF )
        version_group.append( version_area ) # Subgroup for text scaling
        welcome_group.append( version_group )
        print( "showing basic welcome screen")
    return welcome_group

def filename_check():
    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")

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 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 read_time(hardware_clock):
    null_time = time.struct_time(( 2020,  01,   01,   00,  00,  00,   0,   -1,    -1 ))
    try:
        timenow = hardware_clock.datetime
        if timenow.tm_wday not in range ( 0, 7 ):
            hardware_clock.datetime = null_time
        timenow = hardware_clock.datetime
        return timenow
    except:
        return null_time

##
## def methane [later]
##

## def particulates counter
def calculate_aqi_p( p25_reading, p100_reading ):
    gc.collect()
    p25_break_points = (0, 12, 12.1, 35.4, 35.5, 55.4, 55.5, 150.4, 150.5, 250.4, 250.5, 350.4, 350.5, 500.4)
    #print( p25_break_points )
    p100_break_points = (0, 54, 55, 154, 155, 254, 255, 354, 355, 424, 425, 504, 505, 604)
    #print( p100_break_points )
    AQI_levels = (0, 50, 51, 100, 101, 150, 151, 200, 201, 300, 301, 400, 401, 500)
    #print( AQI_levels )
    p25_index = 0
    p100_index = 0
    for i in range (len(AQI_levels)-1):
        if (p25_reading > p25_break_points[i] and p25_reading < p25_break_points[i+1]):
            p25_index = i
    #aqi = int((((ihi-ilo)/(bhi-blo))*(reading-blo))+ilo)
    aqi25 = int((((AQI_levels[p25_index+1] - AQI_levels[p25_index])/(p25_break_points[p25_index+1] - p25_break_points[p25_index]))*(p25_reading-p25_break_points[p25_index]))+AQI_levels[p25_index])
    #print(aqi25)
    for n in range (len(AQI_levels)-1):
        if (p100_reading > p100_break_points[i] and p100_reading < p100_break_points[i+1]):
            p100_index = i
    aqi100 = int((((AQI_levels[p100_index+1] - AQI_levels[p100_index])/(p100_break_points[p100_index+1] - p100_break_points[p100_index]))*(p100_reading-p25_break_points[p100_index]))+AQI_levels[p100_index])
    #print(aqi100)
    #print( p25_break_points[p25_index], p25_break_points[p25_index+1], AQI_levels[p25_index], AQI_levels[p25_index+1], p100_break_points[p100_index], p100_break_points[p100_index+1], AQI_levels[p100_index], AQI_levels[p100_index+1])
    if aqi100 > aqi25:
        return aqi100
    else:
        return aqi25

def initialize_particulates_counter( i2c_bus ):
    particulates_counter = Null_Particulates_Counter()
    try:
        particulates_counter = Particulates_Counter( i2c_bus )
    except:
        pass
    return particulates_counter

class Particulates_Counter( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "particulates_counter", pn = "pmsa003i", address = 0x12, swob = PM25_I2C( com_bus, reset_pin = None ))
        self.aqip = None
        self.pm100 = None
        self.pm25 = None

    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        try:
            self.data = self.swob.read()
        except RuntimeError as err:
            self.data = None
            print( err )
        if self.data is not None:
            self.aqip = calculate_aqi_p( self.data["pm25 standard"], self.data["pm100 standard"] )
        self.pm25 = self.data["pm25 standard"]
        self.pm100 = self.data["pm100 standard"]
    def header(self):
        pass
        #return ""
    def log(self):
        self.datastring = "{}, {}, {}, {}, {}, {}, {}, {}, {}, {}".format(
                self.aqip,
                self.data["pm10 standard"],
                self.data["pm25 standard"],
                self.data["pm100 standard"],
                self.data["particles 03um"],
                self.data["particles 05um"],
                self.data["particles 10um"],
                self.data["particles 25um"],
                self.data["particles 50um"],
                self.data["particles 100um"])
        return self.datastring
    def printlog(self):
        print( self.log())

class Null_Particulates_Counter():
    def __init__( self ):
        self.swob = None
        self.aqip = None
        self.pm100 = None
        self.pm25 = 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 co2
def initialize_co2_photoacoustic( i2c_bus ):
    co2_photoacoustic = Null_CO2_Photoacoustic()
    try:
        co2_photoacoustic = CO2_Photoacoustic( i2c_bus )
    except:
        pass
    return co2_photoacoustic

class CO2_Photoacoustic( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "co2_photoacoustic", pn = "scd40", address = 0x62, swob = adafruit_scd4x.SCD4X( com_bus ))
        if self.swob:
            self.swob.start_periodic_measurement()
        self.temperature_C = None
        self.humidity = None
        self.co2_ppm = None
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        self.co2_ppm = self.swob.CO2
        self.co2_uncty_ppm = 50 + self.co2_ppm * 0.05
        self.temperature_C = self.swob.temperature
        self.humidity = self.swob.relative_humidity
    def header(self):
        return "CO2_ppm, CO2_uncty_ppm, temp_C, RH_percent"
    def log(self):
        return "{}, {}, {}, {}".format( self.co2_ppm, self.co2_uncty_ppm, self.temperature_C, self.humidity )
    def printlog(self):
        print( self.log())

class Null_CO2_Photoacoustic():
    def __init__( self ):
        self.swob = None
        self.temperature_C = None
        self.humidity = None
        self.co2_ppm = 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 high accuracy air temp
def initialize_high_accuracy_air_thermometer( i2c_bus ):
    high_accuracy_air_thermometer = Null_High_Accuracy_Air_Thermometer()
    try:
        high_accuracy_air_thermometer = mcp9808_High_Accuracy_Air_Thermometer( i2c_bus )
    except:
        pass
    return high_accuracy_air_thermometer

class mcp9808_High_Accuracy_Air_Thermometer( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "high_accuracy_air_thermometer", pn = "mcp9808", address = 0x18, swob = adafruit_mcp9808.MCP9808( com_bus ))
        self.temperature_C = None
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        self.temperature_C = self.swob.temperature
    def header(self):
        return "air_temp_C"
    def log(self):
        return "{}".format( self.temperature_C )
    def printlog(self):
        print( self.log())

class Null_High_Accuracy_Air_Thermometer():
    def __init__( self ):
        self.swob = None
        self.temperature_C = 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_lidar_4m_range( i2c_bus ):
    lidar_4m_range = Null_Lidar_4m_Range()
    lidar_4m_range = adafruit_vl53l1x.VL53L1X( i2c_bus )
    lidar_4m_range.start_ranging()
    lidar_4m_range.clear_interrupt()
    #print("Distance: {} m".format(lidar_4m_range.distance/100))
    #lidar_4m_range = Lidar_4m_Range( i2c_bus )

    #try:
    #    lidar_4m_range = Lidar_4m_Range( i2c_bus )
    #    print("init success" )
    #except:
    #    pass
    #    print( "init fail")
    return lidar_4m_range

class Lidar_4m_Range( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "lidar_4m_range", pn = "vl53l1x", address = 0x29, swob = adafruit_vl53l1x.VL53L1X( com_bus ))
        #self.model_id, self.module_type, self.mask_rev = self.swob.model_info
        #self.swob.distance_mode = 2 # long distance mode
        #self.swob.timing_budget = 100

        #print("Model ID: 0x{:0X}".format(self.model_id))
        #print("Module Type: 0x{:0X}".format(self.module_type))
        #print("Mask Revision: 0x{:0X}".format(self.mask_rev))

        #self.swob.start_ranging()
        #vl53.clear_interrupt()
        #self.swob("Distance: {} m".format(self.swob.distance/100))

    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        pass
        #print("reading lidar")
        #if self.swob.data_ready:
        #    self.range_m = self.swob.distance/100
        #    self.swob.clear_interrupt()
        #else:
        #    self.range_m = 0
    def header(self):
        return "range_m"
    def log(self):
        return "{}".format( self.range_m )
    def printlog(self):
        print( self.log())

class Null_Lidar_4m_Range():
    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_tir_surface_thermometer( i2c_bus ):
    tir_surface_thermometer = Null_TIR_Surface_Thermometer()
    try:
        tir_surface_thermometer = mlx90614_TIR_Surface_Thermometer( i2c_bus )
    except:
        pass
    return tir_surface_thermometer

class mlx90614_TIR_Surface_Thermometer( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "tir_surface_thermometer", pn = "mlx90614", address = 0x5A, swob = adafruit_mlx90614.MLX90614( com_bus ))
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        self.t_surface_C = self.swob.object_temperature
    def header(self):
        return "t_surface_C"
    def log(self):
        return "{}".format( self.t_surface_C )
    def printlog(self):
        print( self.log())

class Null_TIR_Surface_Thermometer():
    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_uv_spectrometer( i2c_bus ):
    uv_spectrometer = Null_UV_Spectrometer()
    try:
        uv_spectrometer = as7331_UV_Spectrometer( i2c_bus )
    except ValueError as err:
        #print( "uv spectrometer failed to initialize: {}".format(err))
        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.bands = (360, 300, 260)
        self.dict_chip_n = { 360:"as7331", 300:"as7331", 260:"as7331" }
        self.dict_bw_fwhm_nm = { 360:80, 300:40, 260:40 } #see datasheet
        #https://look.ams-osram.com/m/1856fd2c69c35605/original/AS7331-Spectral-UVA-B-C-Sensor.pdf
        self.afov_deg = (10 * 2)
        self.fcal_unct_percent = 0 # no reported value
    def found(self):
        print("found", self.pn, self.swob)

    def check_gain_ratio(self):
        gain_callout = self.swob.gain
        if gain_callout == 11:
            gain_ratio = 1
        elif gain_callout == 10:
            gain_ratio = 2
        elif gain_callout == 9:
            gain_ratio = 4
        elif gain_callout == 8:
            gain_ratio = 8
        elif gain_callout == 7:
            gain_ratio = 16
        elif gain_callout == 6:
            gain_ratio = 32
        elif gain_callout == 5:
            gain_ratio = 64
        elif gain_callout == 4:
            gain_ratio = 128
        elif gain_callout == 3:
            gain_ratio = 256
        elif gain_callout == 2:
            gain_ratio = 512
        elif gain_callout == 1:
            gain_ratio = 1024
        elif gain_callout == 0:
            gain_ratio = 2048
        return gain_ratio

    def set_gain_number(self, gain_number):
        if gain_number in range (0,12):
            if gain_number == 11:
                gain_constant = as7331.GAIN_2048X
            if gain_number == 10:
                gain_constant = as7331.GAIN_1024X
            if gain_number == 9:
                gain_constant = as7331.GAIN_512X
            if gain_number == 8:
                gain_constant = as7331.GAIN_256X
            if gain_number == 7:
                gain_constant = as7331.GAIN_128X
            if gain_number == 6:
                gain_constant = as7331.GAIN_64X
            if gain_number == 5:
                gain_constant = as7331.GAIN_32X
            if gain_number == 4:
                gain_constant = as7331.GAIN_16X
            if gain_number == 3:
                gain_constant = as7331.GAIN_8X
            if gain_number == 2:
                gain_constant = as7331.GAIN_4X
            if gain_number == 1:
                gain_constant = as7331.GAIN_2X
            if gain_number == 0:
                gain_constant = as7331.GAIN_1X
            self.swob.gain = gain_constant
        else:
            print( "out of range: set gain number to 0-11 to get gain_ratios from 1 to 2048" )

    def set_integration_time( self, intg_number ):
        if intg_number == 0:
            self.integration_time = as7331.INTEGRATION_TIME_1MS
            intg_time_ms = 1
        if intg_number == 1:
            self.integration_time = as7331.INTEGRATION_TIME_2MS
            intg_time_ms = 2
        if intg_number == 2:
            self.integration_time = as7331.INTEGRATION_TIME_4MS
            intg_time_ms = 4
        if intg_number == 3:
            self.integration_time = as7331.INTEGRATION_TIME_8MS
            intg_time_ms = 8
        if intg_number == 4:
            self.integration_time = as7331.INTEGRATION_TIME_16MS
            intg_time_ms = 16
        if intg_number == 5:
            self.integration_time = as7331.INTEGRATION_TIME_32MS
            intg_time_ms = 32
        if intg_number == 6:
            self.integration_time = as7331.INTEGRATION_TIME_64MS
            intg_time_ms = 64
        if intg_number == 7:
            self.integration_time = as7331.INTEGRATION_TIME_128MS
            intg_time_ms = 128
        if intg_number == 8:
            self.integration_time = as7331.INTEGRATION_TIME_256MS
            intg_time_ms = 256
        if intg_number == 9:
            self.integration_time = as7331.INTEGRATION_TIME_512MS
            intg_time_ms = 512
        if intg_number == 10:
            self.integration_time = as7331.INTEGRATION_TIME_1024MS
            intg_time_ms = 1024
        if intg_number == 11:
            self.integration_time = as7331.INTEGRATION_TIME_2048MS
            intg_time_ms = 2048
        if intg_number == 12:
            self.integration_time = as7331.INTEGRATION_TIME_4096MS
            intg_time_ms = 4096
        if intg_number == 13:
            self.integration_time = as7331.INTEGRATION_TIME_8192MS
            intg_time_ms = 8192
        if intg_number == 14:
            self.integration_time = as7331.INTEGRATION_TIME_16384MS
            intg_time_ms = 16384
        return intg_time_ms

    def read(self):
        self.UVA_counts, self.UVB_counts, self.UVC_counts, self.chip_temp_c_counts = self.swob.raw_values
        self.UVA, self.UVB, self.UVC, self.chip_temp_c = self.swob.values
        self.dict_fcal = {360:self.UVA, 300:self.UVB, 260:self.UVC}
        self.dict_counts = {360:self.UVA_counts, 300:self.UVB_counts, 260:self.UVC_counts}
    def header(self):
        return "sensorPN, Wl.nm, raw_counts, irrad.stella.cal, irrad.stella.uncty, irrad_factory.cal, irrad_factory.uncty, gain, integration_time_ms, chip_temp_C"
        #return "UVC.WL.nm, UVC_uncal, UVB.WL.nm, UVB_uncal, UVA.WL.nm, UVA_uncal, UVS.temp.C"
    def log(self):
        self.dataframe = "as7331, 260, {}, \n".format( self.UVC ) # add columns for counts, cals, gain, intg, temp
        self.dataframe += "as7331, 300, {}, \n".format( self.UVB ) # add columns for counts, cals, gain, intg, temp
        self.dataframe += "as7331, 360, {}".format( self.UVA ) # add columns for counts, cals, gain, intg, temp
        return self.dataframe
    def printlog(self):
        print( self.log())

class Null_UV_Spectrometer():
    def __init__( self ):
        self.swob = None
        self.dict_chip_n = {0:0}
    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 check_gain_ratio(self):
        pass

def initialize_vis_nir_triad_spectrometer( i2c_bus ):
    triad_spectrometer = Null_Triad_Spectrometer()
    try:
        triad_spectrometer = as7265x_Triad_Spectrometer( i2c_bus )
        #print( "triad spectrometer initialized" )
    except:
        #print( "triad spectrometer failed to initialize" )
        pass
    return triad_spectrometer

class as7265x_Triad_Spectrometer( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "vis_nir_triad_spectrometer", pn = "as7256x", address = 0x49, swob = AS7265X( com_bus ))
        if self.swob:
            self.swob.disable_indicator()
            self.swob.set_measurement_mode(AS7265X_sparkfun.MEASUREMENT_MODE_6CHAN_CONTINUOUS)
            self.bands = 610, 680, 730, 760, 810, 860, 560, 585, 645, 705, 900, 940, 410, 435, 460, 485, 510, 535
            self.chip_n = 1,   1,   1,   1,   1,   1,   2,   2,   2,   2,   2,   2,   3,   3,   3,   3,   3,   3
            self.dict_chip_n = {key:value for key, value in zip(self.bands, self.chip_n )}
            self.bands_sorted = sorted( self.bands )
            self.uncertainty_percent = 12
            self.bw_fwhm_nm = 20
            self.afov_deg = (20.5 * 2) #datasheet reports half angle.
    def check_gain_ratio(self):
        gain_number = self.swob._gain
        if gain_number < 1:
            gain_ratio = 1
        elif gain_number == 1:
            gain_ratio = 3.7
        elif gain_number == 2:
            gain_ratio = 16
        elif gain_number == 3:
            gain_ratio = 64
        return gain_ratio
    def set_gain_number(self, gain_number):
        if gain_number in range (0,4):
            self.swob.set_gain( gain_number )
        else:
            print( "out of range: set gain number to 0-3 to get gain_ratios of 1, 3.7, 16, 64" )
    def set_integration_cycles( self, cycles ):
        if cycles in range (0, 256):
            self.swob.set_integration_cycles(cycles)
            intg_time_ms = int(round((2.8*(cycles+1)),0))
        else:
            print( "out of range: set integration cycles to 0-255 for 0-717ms integration time." )
            intg_time_ms = None
        return intg_time_ms
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        self.chip_temp_c = {1:self.swob.get_temperature(1), 2:self.swob.get_temperature(2), 3:self.swob.get_temperature(3)}
        self.data_counts = self.swob.get_value(0) # 0th position raw counts, bands unsorted order
        # VV create a dictionary where key = WL and value = raw counts
        self.dict_counts = {key:value for key, value in zip(self.bands, self.data_counts)}
        self.data_fcal = self.swob.get_value(1) # 1th position factory calibrated irrad value, bands unsorted order
        # VV create a dictionary where key = WL and value = factory cal irradiance
        self.dict_fcal = {key:value for key, value in zip(self.bands, self.data_fcal)}
        #self.dict_uncty_fcal = {key:value for key, value in zip(self.bands, (self.data_fcal*self.uncert_percent/100))}
        # >> self.dict_scal = {key:value for key, value in zip(self.bands, 0)}
        #self.dict_uncty_scal = {key:value for key, value in zip(self.bands, (0))}
    def list_channels():
        return self.bands_sorted
    def header( self ):
        return "WL.nm, irrad.uW/(cm^2), irrad.uncty.uW/(cm^2), counts, chip_num, chip_temp_C"
    def log( self ):
        print( self.data_counts )
        print( self.data_fcal )
        #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) )
    def lamps_on(self):
        print( "turn on the lamps")
        self.swob.enable_bulb(0)   # white
        self.swob.enable_bulb(1)   # NIR
        self.swob.enable_bulb(2)   # UV
    def lamps_off(self):
        print( "turn off the lamps")
        self.swob.disable_bulb(0)   # white
        self.swob.disable_bulb(1)   # NIR
        self.swob.disable_bulb(2)   # UV

class Null_Triad_Spectrometer():
    def __init__( self ):
        self.swob = None
        self.bands = None
        self.bands_sorted = [0,0]   # empty list
        self.dict_chip_n = [0,0]
        self.chip_temps = [0,0]
        self.dict_fcal = {0:0}      # empty dictionary
        self.dict_counts = {0:0}
        self.uncert_percent = 10
    def check_gain_ratio(self):
        pass
    def set_gain_number(self, gain_number):
        pass
    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 lamps_on(self):
        pass
    def lamps_off(self):
        pass
    def set_integration_cycles(self):
        pass

def initialize_ambient_air_sensor( i2c_bus ):
    ambient_air_sensor = Null_Air_Sensor()
    try:
        ambient_air_sensor = bme280_Air_Sensor( i2c_bus )
    except:
        pass
    return ambient_air_sensor

class bme280_Air_Sensor( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "ambient_air_sensor", pn = "bme280", address = 0x77, swob = adafruit_bme280.Adafruit_BME280_I2C( com_bus ))
        self.temperature_C = None
        self.pressure = None
        self.altitude = None
        self.humidity = None
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        self.temperature_C = self.swob.temperature
        self.pressure = self.swob.pressure
        self.humidity = self.swob.relative_humidity
        self.altitude = self.swob.altitude

        # TD: =243.04*(LN(RH/100)+((17.625*T)/(243.04+T)))/(17.625-LN(RH/100)-((17.625*T)/(243.04+T)))
        # from https://bmcnoldy.earth.miami.edu/Humidity.html

        self.dewpoint = 0 #self.temperature -((100-self.humidity)/5) #update this to the formula above.
        self.dp_uncty = 3.2
    def log(self):
        # name, units, value, +/-, uncertainty ## per datasheet
        return "{}, {}, {}, {}, {}".format(
            round(self.temperature_C, 3), round(self.humidity, 1) , round(self.dewpoint, 1) , self.pressure, round(self.altitude, 3) )
    def printlog(self):
        print( self.log())
    def header(self):
        return( "air_temp_C, air_temp_uncty_C, air_humid_%, air_humid_uncty_%, air_dewpoint_C, air_dp_uncty_C, air_press_hPa, air_press_uncty_hPa, press_alt_m, press_alt_uncty_m" )

class Null_Air_Sensor(Device):
    def __init__( self ):
        self.swob = None
        self.temperature_C = None
        self.pressure = None
        self.altitude = None
        self.humidity = 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_gps( uart ):
    gps = Null_GPS()
    try:
        gps = pa1616d_GPS( uart )
    except:
        print("gps failed init")
        pass
    return gps

class pa1616d_GPS( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "gps", pn = "pa1616d", address = 0x00, swob = adafruit_gps.GPS( 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.longitude = self.swob.longitude
        self.altitude = self.swob.altitude_m
        self.timestruct = self.swob.timestamp_utc
    def header(self):
        return( "lat.deg.N, long.deg.W, alt.m" )
    def log(self):
        if self.timestruct is not None:
            self.gps_timestamp = "{}{:02}{:02}T{:02}{:02}{:02}Z".format(
                        self.timestruct.tm_year,# Note you might not get all data like year month day
                        self.timestruct.tm_mon,
                        self.timestruct.tm_mday,
                        self.timestruct.tm_hour,
                        self.timestruct.tm_min,
                        self.timestruct.tm_sec
                        )
        else: self.gps_timestamp = None #"20000101T000000Z"
        return "{}, {}, {}, {}".format( self.latitude, self.longitude, self.altitude, self.gps_timestamp )
    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_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( round( self.voltage, 3 ), round( self.percentage, 1 ) )
    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 initialize_hardware_clock( i2c_bus ):
    # 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( "hardware clock initialized" )
        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 hardware clock" )
    except:
        print( "failed to set system clock" )
    DAYS = { 0:"Sunday", 1:"Monday", 2:"Tuesday",
        3:"Wednesday", 4:"Thursday", 5:"Friday", 6:"Saturday" }
    return hardware_clock

def initialize_encoder( pinA, pinB ):
    try:
        encoder = rotaryio.IncrementalEncoder( pinA, pinB )
        print( "rotary encoder initialized" )
    except:
        encoder = False
    return encoder

def initialize_encoder_button( DIOpin ):
    try:
        button = digitalio.DigitalInOut( DIOpin )
        button.direction = digitalio.Direction.INPUT
        button.pull = digitalio.Pull.UP
        print( "encoder button initialized" )
    except:
        button = False
    return button

def initialize_touch_screen( i2c_bus ):
    touch_screen = Null_Touch_Screen()
    try:
        touch_screen = Focal_Touch_Screen( i2c_bus )
    except:
        pass
    return touch_screen

class Focal_Touch_Screen( Device ):
    def __init__( self, com_bus ):
        super().__init__(name = "touch_screen", pn = "FocalTouch", address = 0x38, swob = adafruit_focaltouch.Adafruit_FocalTouch(com_bus, debug=False))
    def found(self):
        print("found", self.pn, self.swob)
    def read(self):
        self.is_touched = self.swob.touched
        if self.is_touched:
            self.dict = self.swob.touches
            self.tx = 320 - self.dict[0]['y'] #transform
            self.ty = self.dict[0]['x'] #transform
    def log(self):
        pass
    def printlog(self):
        print( self.log())

class Null_Touch_Screen(Device):
    def __init__( self ):
        self.swob = None
        #self.swob.is_touched = False
    def found(self):
        pass
    def read(self):
        pass
    def log(self):
        pass
    def report(self):
        pass
    def printlog(self):
        pass

def initialize_display( spi_bus ):
    try:
        # displayio/dafruit_ili9341 library owns the pins until display release
        displayio.release_displays()
        tft_dc = board.D9
        tft_rst = board.D10
        tft_cs = board.D11
        display_bus = displayio.FourWire(spi_bus, command=tft_dc, chip_select=tft_cs, reset=tft_rst)
        display = adafruit_ili9341.ILI9341(display_bus, width=320, height=240, rotation=0)
        print( "display initialized")
        return display
    except ValueError as err:
        print("Error: display failed to initialize {:}".format(err))
        return False

def initialize_uart( txpin, rxpin ):
    try:
        uart = busio.UART(txpin, rxpin, baudrate=9600, timeout=10)
        print( "uart bus initialized" )
    except:
        uart = False
    return uart

def initialize_i2c_bus( SCL_pin, SDA_pin ):
    try:
        i2c_bus = busio.I2C( SCL_pin, SDA_pin, frequency=100000 )
        # limit bus speed to 100kHz to work with tir_surface_thermometer
        print( "i2c bus initialized " )
    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( spi_bus, neopix ):
    fault_color = ( 102, 255, 0 ) #orange
    try:
        sd_cs = board.A3
        sdcard = sdcardio.SDCard( spi_bus, sd_cs )
        vfs = storage.VfsFat( sdcard )
        storage.mount( vfs, "/sd" )
        print( "sd card initialized" )
    except( OSError, ValueError ) as err:
        print( "No SD card found, or card is full: {}".format(err) )
        vfs = False
        sdcard = False
        neopix.fill( fault_color )
        time.sleep(2)
    return vfs, sdcard

def initialize_spi_bus():
    try:
        spi_bus = board.SPI()
        print( "SPI bus initialized" )
    except:
        spi_bus = False
        print( "SPI bus failed to initialize" )
    return spi_bus

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 get_analog_voltage(pin):
    return (pin.value * 3.3) / 65536 * 2 # factor of two because divide by 2 voltage dividers on inputs

def initialize_output_pin( pin ):
    opin = digitalio.DigitalInOut( pin )
    opin.direction = digitalio.Direction.OUTPUT
    print( "output pin initialized" )
    #except:
    #    print( "output pin failed to initialize" )
    #    opin = False
    return opin

def memory_check( message ):
    gc.collect()
    mem_free_kB = gc.mem_free()/1000
    #int((100* (mem_free_kB)/start_mem_free_kB
    print( "{} memory free: {} kB, {} %".format( message, int(mem_free_kB), int((100* (mem_free_kB)/2000 ))))

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

gc.collect()
main()
