SOFTWARE_VERSION_NUMBER = "6.6.4"
# STELLA-1 instrument code
# Science and Technology Education for Land/ Life Assessment
# Paul Mirel 2023-04-16

import time
import board
import busio

with busio.I2C(board.SCL, board.SDA) as i2c:
    i2c.try_lock()
    list = [hex(device_address) for device_address in i2c.scan()]
    li_present = '0x29' in list
    print( "li_present", li_present )
    alt_display_present = '0x48' in list
    print( "alt_display_present", alt_display_present )
    i2c.unlock()

HUGE_VALUE = False #99999 #False #654321 # sustitute for all irradiance values, to stress the available processor memory.
# 54321 pretty reliably causes a fail on second command to sample/average.
# 654321 causes fail sometimes after first iteration of sample/average
# 7654321 reliably causes a fail right away
# change HUGE_VALUE to False to allow ordinary measurements of irradiance
MAX_VALUE = 99999 #cap irradiance values at 100,000 - 1 to prevent memory allocation crash

# import system libraries
import gc #garbage collection, RAM management
gc.collect()
print("start memory free {} B".format( gc.mem_free() ))
last_alloc = gc.mem_alloc()
import os
import sys
import microcontroller
import rtc
import digitalio
import terminalio
import storage
import sdcardio
from analogio import AnalogIn

# import display libraries
import displayio
import vectorio # for shapes
import adafruit_ili9341 # TFT (thin film transistor) display
from adafruit_display_text.bitmap_label import Label
from adafruit_display_text import label
from adafruit_display_shapes.rect import Rect
from adafruit_display_shapes.circle import Circle
#from adafruit_display_shapes.triangle import Triangle
#from adafruit_display_shapes.line import Line
if alt_display_present:
    import adafruit_tsc2007
else:
    from adafruit_stmpe610 import Adafruit_STMPE610_SPI # touch screen reader


# import device specific libraries
import adafruit_mlx90614    # thermal infrared

import adafruit_as726x      # visible spectrum
import adafruit_pcf8523     # real time hardware_clock

if li_present:
    import adafruit_vl53l4cd
else:
    import adafruit_mcp9808     # air temperature
    from adafruit_bme280 import basic as adafruit_bme280 # weather

import ulab
print("memory used for loading libraries {} kB".format( (last_alloc - gc.mem_alloc()) /1000))

gc.collect()

SCREENSIZE_X = 320
SCREENSIZE_Y = 240
VIS_BANDS = ( 450, 500, 550, 570, 600, 650 ) # from amd as7262 datasheet
VIS_BAND_PREFIXES = ( "V", "B",  "G", "Y", "O", "R" )
NIR_BANDS = ( 610, 680, 730, 760, 810, 860 ) # from amd as7263 datasheet
ON = 0  #sample_indicator is active low.
OFF = 1
DATA_FILE = "/sd/data.csv"
RECORD = 0
PAUSE =  1
number_of_record_pause_states = 2

RECORD_PAUSE =  0
AVERAGE =       2
SINGLE =        1

def main():
    # set  constants
    PUSHBUTTON_IO_PIN = board.D12
    LOW_BATTERY_VOLTAGE = 3.1

    HOLD = 1
    LIVE = 0

    CREATE = 0
    UPDATE = 1
    operational = True
    startup = True

    # initialize bus
    i2c_bus = initialize_i2c_bus( board.SCL, board.SDA )
    spi_bus = initialize_spi_bus()
    uart_bus = initialize_uart_bus( board.TX, board.RX )

    sample_indicator_LED = initialize_sample_indicator( board.A0 )
    sample_indicator_LED.value = ON
    time.sleep( 0.1 )
    sample_indicator_LED.value = OFF
    UID = int.from_bytes(microcontroller.cpu.uid, "big") % 10000
    print("unique identifier (UID) == {0}".format( UID ))
    try:
        main_battery_monitor = initialize_AnalogIn( board.VOLTAGE_MONITOR )
    except AttributeError:
        main_battery_monitor = initialize_AnalogIn( board.A1 )
    main_battery_voltage = check_battery_voltage( main_battery_monitor )
    print( "main_battery_voltage == {}".format( main_battery_voltage ))
    pushbutton = initialize_pushbutton( PUSHBUTTON_IO_PIN )
    sdcard = initialize_sdcard( spi_bus )   # it is important that the SD card be initialized
                                            # before accessing any other peripheral on the bus.
                                            # Failure to do so can prevent the SD card from being
                                            # recognized until it is powered off or re-inserted.
    # initialize the display, and form the large graphics blocks in memory,
    # so that memory fragmentation, later on, when it is nearly full does not
    # result in not enough available
    display = initialize_display( spi_bus )
    if alt_display_present:
        touch_screen = initialize_alt_touch_screen( i2c_bus )
    else:
        touch_screen = initialize_touch_screen( spi_bus )
    if touch_screen:
        print( "Display detected." )
        drone_mode = False
        display_group_table = initialize_display_group( display )
        text_group = create_table_screen( display, display_group_table )
        gc.collect()
        welcome_group = create_welcome_screen( display )
        display.show( welcome_group )
        time.sleep(1)
    else:
        print( "Display not detected. Operating without display." )
        drone_mode = True
    gc.collect()
    # initialize real time hardware clock, and use it as the source for the microcontroller real time clock
    hardware_clock, hardware_clock_battery_OK = initialize_real_time_clock( i2c_bus )
    system_clock = rtc.RTC()
    system_clock.datetime = hardware_clock.datetime
    # initialize sd card file storage

    write_header = initialize_data_file()
    print( "write header == {}".format( write_header ))
    gc.collect()
    # check config file
    desired_sample_interval_s, averaging_number_of_samples, averaging_count_down, count_down_step_s = check_STELLA_config()
    # desired_sample_interval_s = 5
    ###
    TIR_sensor = initialize_TIR_sensor( i2c_bus )
    VIS_sensor = initialize_VIS_sensor( i2c_bus )
    NIR_sensor = initialize_NIR_sensor( uart_bus )
    LiDAR_sensor = initialize_LiDAR_sensor( i2c_bus )
    if LiDAR_sensor:
        Li_present = True
        text_group[6].text = "Range:"
        text_group[6].x = text_group[6].x - 10
        WX_sensor = False
        AT_sensor = False
    else:
        Li_present = False
        AT_sensor = initialize_AT_sensor( i2c_bus )
        WX_sensor = initialize_WX_sensor( i2c_bus )
    gc.collect()
    if not drone_mode:
        display.show( display_group_table )

    # set instrument mode
    instrument_mode = RECORD_PAUSE
    last_instrument_mode = SINGLE
    record_pause_state = RECORD
    last_record_pause_state = PAUSE
    write_data = True
    read_data = True
    mirror_data = True

    # set input variables
    toggle_record_pause_pressed = False
    last_toggle_record_pause_pressed = False
    sample_one_pressed = False
    last_sample_one_pressed = False
    sample_and_average_pressed = False
    last_sample_and_average_pressed = False
    pushbutton_press_state = False
    pushbutton_last_press_state = False

    if not drone_mode:
        create_telltale( display_group_table )
        create_or_update = CREATE
        show_record_pause_icon( display_group_table, record_pause_state, create_or_update )
        create_or_update = UPDATE
        gc.collect()
    mem_free_before_discard = gc.mem_free()/1000
    #print( "mem free before discarding welcome group == {} kB".format( mem_free_before_discard ))
    #print( "discard welcome group here" )
    if not drone_mode:
        welcome_group_length = len(welcome_group)
        #print( "length of welcome group is {}".format( welcome_group_length ))
        for i in range (0, welcome_group_length):
            welcome_group.pop()
            print( "pop" )
        del welcome_group
    gc.collect()
    #mem_free_after_discard = gc.mem_free()/1000
    #mem_saved = mem_free_after_discard - mem_free_before_discard
    #print( "memory saved by discarding welcome group == {} kB".format( mem_saved ))

    cycle_count = 0
    gc.collect()
    while operational:
        print( "\ncycle count == {}\n".format( cycle_count))
        cycle_count += 1
        exit_wait = False
        gc.collect()
        memory_free = gc.mem_free()
        print( "memory free = {} kB".format( memory_free/1000 ))
        last_sample_time_s = time.monotonic()
        if not drone_mode:
            gc.collect()
            show_record_pause_icon( display_group_table, record_pause_state, create_or_update )
            show_telltale( display_group_table, instrument_mode )

        if read_data or startup:
            gc.collect()
            startup = False
            timestamp = hardware_clock.datetime
            if instrument_mode == RECORD_PAUSE:
                #print( "instrument mode == RECORD_PAUSE" )
                if last_instrument_mode != RECORD_PAUSE:
                    batch_number = update_batch( timestamp )
                    last_instrument_mode = RECORD_PAUSE
                    exit_wait = 1
                    iso8601_utc = "{:04}{:02}{:02}T{:02}{:02}{:02}Z".format( timestamp.tm_year, timestamp.tm_mon, timestamp.tm_mday, timestamp.tm_hour, timestamp.tm_min, timestamp.tm_sec)
            #format( iso8601_utc )
            decimal_hour = timestamp_to_decimal_hour( timestamp )
            surface_temperature_C = read_TIR_sensor( TIR_sensor )
            air_temperature_C = read_AT_sensor( AT_sensor )

            visible_irradiance_uW_per_cm_squared = read_spectrum( VIS_sensor )
            gc.collect()
            nir_irradiance_uW_per_cm_squared = read_NIR_sensor( NIR_sensor )
            if Li_present:
                range_m = read_LiDAR_sensor( LiDAR_sensor )
                humidity_relative_percent = 0
                barometric_pressure_hPa = 0
                air_temperature_C = 0
            else:
                humidity_relative_percent, barometric_pressure_hPa = read_WX_sensor( WX_sensor )
                air_temperature_C = read_AT_sensor( AT_sensor )
                range_m = 0
            main_battery_voltage = check_battery_voltage( main_battery_monitor )
            if main_battery_voltage < LOW_BATTERY_VOLTAGE:
                low_battery_voltage_notification( text_group )
            if not drone_mode:
                populate_table_values( text_group, UID, timestamp, decimal_hour, batch_number, visible_irradiance_uW_per_cm_squared,
                    nir_irradiance_uW_per_cm_squared, surface_temperature_C, air_temperature_C, range_m, Li_present )

        gc.collect()
        if mirror_data:
            mirror_data_over_usb(
                UID, timestamp, decimal_hour, batch_number,
                visible_irradiance_uW_per_cm_squared, nir_irradiance_uW_per_cm_squared,
                surface_temperature_C, air_temperature_C, humidity_relative_percent, range_m
                )
            if not read_data:
                time.sleep( 0.2 )
        gc.collect()
        if write_data:
            write_line_success = write_data_to_file( UID, timestamp, decimal_hour, batch_number, range_m,
                visible_irradiance_uW_per_cm_squared, nir_irradiance_uW_per_cm_squared,
                surface_temperature_C, air_temperature_C, humidity_relative_percent,
                barometric_pressure_hPa,
                main_battery_voltage, sample_indicator_LED )
        gc.collect()
        if not drone_mode:
            sample_one_pressed, toggle_record_pause_pressed, sample_and_average_pressed = screen_pressed( touch_screen )
            if sample_one_pressed or toggle_record_pause_pressed or sample_and_average_pressed:
                exit_wait = True
            # check for toggle_record_pause_pressed rising edge
            if toggle_record_pause_pressed:
                if not last_toggle_record_pause_pressed:
                    toggle_rising_edge = True
                else:
                    toggle_rising_edge = False
                last_toggle_record_pause_pressed = True
            else:
                last_toggle_record_pause_pressed = False

            # SINGLE mode ------------------------------ screen button behavior check pass -------------------------------
            if sample_one_pressed:
                gc.collect()
                exit_wait = True
                instrument_mode = SINGLE
                last_instrument_mode = SINGLE
                record_pause_state = PAUSE
                last_record_pause_state = PAUSE
                read_data = True #False
                write_data = False

            # AVERAGE mode ------------------------------ screen button behavior check pass -------------------------------
            if sample_and_average_pressed:
                gc.collect()
                exit_wait = True
                instrument_mode = AVERAGE
                last_instrument_mode = AVERAGE
                record_pause_state = PAUSE
                last_record_pause_state = PAUSE
                read_data = True
                write_data = False

            # RECORD_PAUSE mode ------------------------------ screen button behavior check pass -------------------------------
            if toggle_record_pause_pressed:
                gc.collect()
                instrument_mode = RECORD_PAUSE
                if toggle_rising_edge:
                    if last_instrument_mode != RECORD_PAUSE:
                        batch_number = update_batch( timestamp )
                        last_instrument_mode = RECORD_PAUSE
                        record_pause_state = RECORD
                        read_data = True
                        write_data = True
                        mirror_data = True
                    else:
                        record_pause_state = (record_pause_state + 1) % number_of_record_pause_states
                        if record_pause_state == RECORD:
                            batch_number = update_batch( timestamp )
                            read_data = True
                            write_data = True
                            data = True
                        else:
                            read_data = True
                            write_data = False
                            mirror_data = True
                    last_instrument_mode = RECORD_PAUSE

            # SINGLE mode ------------------------------ pushbutton behavior check pass -------------------------------
            if instrument_mode == SINGLE:
                gc.collect()
                exit_wait = True
                pushbutton_press_state = pushbutton_pressed( pushbutton )
                if pushbutton_press_state:                                  # if the button is being pushed
                    if not pushbutton_last_press_state:                     # if the button was not previously pushed, detect rising edge
                        pushbutton_last_press_state = True                  # do the things to record a point
                        record_pause_state = RECORD
                        batch_number = update_batch( timestamp )
                        write_data = True
                        read_data = True
                    else:
                        record_pause_state = PAUSE                              # don't read new data (hold values), and don't write it.
                        write_data = False
                        read_data = False
                        pushbutton_last_press_state = True
                else:                                                       # if the button is not being pushed
                    pushbutton_last_press_state = False                     # log that it is not pushed
                    record_pause_state = PAUSE
                    last_record_pause_state = record_pause_state            # don't read new data (hold values), and don't write it.
                    write_data = False
                    read_data = False

            # RECORD_PAUSE mode ------------------------------ pushbutton behavior check pass ------------------------------
            if instrument_mode == RECORD_PAUSE:
                gc.collect()
                pushbutton_press_state = pushbutton_pressed( pushbutton ) # if the button is pushed, whatever the current record pause state is...
                if pushbutton_press_state:
                    if not pushbutton_last_press_state and not write_data:
                        batch_number = update_batch( timestamp )
                    record_pause_state = RECORD
                    read_data = True
                    write_data = True
                    pushbutton_last_press_state = True  # set the last press state to "pressed"
                elif pushbutton_last_press_state:       # if the button goes from last pushed to now not pushed; detect falling edge
                    record_pause_state = PAUSE
                    read_data = True
                    write_data = False
                    pushbutton_last_press_state = False # set the last press state to "not pressed"

            # AVERAGE mode ------------------------------ pushbutton behavior check pass -------------------------------
            if instrument_mode == AVERAGE:
                sampling_index = 0
                gc.collect()
                exit_wait = True
                pushbutton_press_state = pushbutton_pressed( pushbutton )
                if pushbutton_press_state:                                  # if the button is being pushed
                    if not pushbutton_last_press_state:                     # if the button was not previously pushed, detect rising edge
                        pushbutton_last_press_state = True                  # do the things to record a point
                        record_pause_state = RECORD
                        batch_number = update_batch( timestamp )
                        print( "take samples and average them, write to special file" )
                        count_down_index = averaging_count_down
                        main_battery_voltage = check_battery_voltage( main_battery_monitor )
                        VIS_sensor_temperature, NIR_sensor_temperature = get_spectral_sensor_temperatures( VIS_sensor, NIR_sensor )
                        header_values = ( UID, batch_number, iso8601_utc, surface_temperature_C, air_temperature_C, humidity_relative_percent, range_m, VIS_sensor_temperature, NIR_sensor_temperature )
                        sampling_filename = create_sampling_file( UID, batch_number, timestamp, decimal_hour, header_values )
                        sampling_group = create_sampling_overlay( display )
                        sampling_text_area = create_sampling_text( count_down_index, count_down_step_s, display )
                        #sampling_overlay.append(sampling_text_group)
                        sampling_index = averaging_number_of_samples
                        #sampling_text_group = displayio.Group( scale=6, x=25, y= 75 )
                        sampling_text = ("SAMPLE\n   {}".format( sampling_index ))
                        gc.collect()
                        print( "Memory free right before create sampling text area == {} kB".format(gc.mem_free()/1000))
                    sample_average = [ 0,0,0,0,0,0,0,0,0,0,0,0 ]
                    gc.collect()
                    while sampling_index > 0:
                        if sampling_index > 9:
                            sampling_text_area.text = ("SAMPLE\n  {}".format( sampling_index ))
                        else:
                            sampling_text_area.text = ("SAMPLE\n   {}".format( sampling_index ))
                        visible_irradiance_uW_per_cm_squared = read_spectrum( VIS_sensor )
                        nir_irradiance_uW_per_cm_squared = read_NIR_sensor( NIR_sensor )
                        write_sampling_line_to_file( UID, batch_number, iso8601_utc,
                            sample_indicator_LED, sampling_filename, sampling_index,
                            visible_irradiance_uW_per_cm_squared, nir_irradiance_uW_per_cm_squared )
                        spectral_samples = [0,0,0,0,0,0,0,0,0,0,0,0]
                        for i, v in enumerate( visible_irradiance_uW_per_cm_squared ):
                            spectral_samples[ i ] = visible_irradiance_uW_per_cm_squared[ i ]
                        for i, v in enumerate( nir_irradiance_uW_per_cm_squared ):
                            spectral_samples[ i + 6] = nir_irradiance_uW_per_cm_squared[ i ]
                        #print( averaging_number_of_samples - sampling_index )
                        print( spectral_samples )
                        for i,v in enumerate( spectral_samples ):
                            sample_average[ i ] = sample_average[ i ] + v
                        sampling_index = sampling_index - 1
                    for i,v in enumerate( sample_average ):
                        sample_average[ i ] = int( round( sample_average[ i ]/ averaging_number_of_samples, 1 ))
                    #print( sample_average )
                    write_tail_of_sampling_file( UID, batch_number, iso8601_utc,
                        sample_indicator_LED, sampling_filename, averaging_number_of_samples,
                        sample_average, VIS_sensor_temperature, NIR_sensor_temperature, surface_temperature_C,
                        air_temperature_C, humidity_relative_percent, range_m
                        )
                    write_line_success = write_data_to_file( UID, timestamp, decimal_hour, batch_number, range_m,
                        visible_irradiance_uW_per_cm_squared, nir_irradiance_uW_per_cm_squared,
                        surface_temperature_C, air_temperature_C, humidity_relative_percent,
                        barometric_pressure_hPa,
                        main_battery_voltage, sample_indicator_LED)
                    gc.collect()
                    '''
                    mirror_data_over_usb(
                        UID, timestamp, decimal_hour, batch_number, range_m,
                        visible_irradiance_uW_per_cm_squared, nir_irradiance_uW_per_cm_squared,
                        surface_temperature_C, air_temperature_C, humidity_relative_percent
                        )
                    '''
                    gc.collect()
                    del sampling_group
                    #del sampling_text_area
                    del header_values
                    del nir_irradiance_uW_per_cm_squared
                    del visible_irradiance_uW_per_cm_squared
                    del sample_average
                    time_begin_sdev = time.monotonic()
                    sampling_text_area.text = ( "ST DEV\n CALC" )
                    gc.collect()
                    print( "make function call to calculate standard deviations, with {} kB free of {} kB total".format( gc.mem_free()/1000, (gc.mem_alloc()+gc.mem_free())/1000 ))
                    success = calculate_and_write_standard_deviations( UID, batch_number, sampling_filename )
                    if success:
                        sampling_text_area.text = ( "SUCCESS\n      " )
                    else:
                        sampling_text_area.text = ( "No SDV\n      " )
                    time.sleep(0.5)
                    time_stop_sdev = time.monotonic()
                    print( "elapsed time to process standard deviation == {} s".format( time_stop_sdev - time_begin_sdev))
                    #stall()
                    write_data = True
                    read_data = True
                    display.show( display_group_table )
                    pushbutton_press_state = False
                    pushbutton_last_press_state = False
                    record_pause_state = PAUSE
                    read_data = True
                    write_data = False
                    mirror_data = True

        remaining_wait_time = desired_sample_interval_s - (time.monotonic() - last_sample_time_s)
        while remaining_wait_time > 0 and not exit_wait:
            # check inputs
            remaining_wait_time = desired_sample_interval_s - (time.monotonic() - last_sample_time_s)
            #print( "remaining_wait_time == {:}".format(remaining_wait_time))
            time.sleep( 0.1 )

    gc.collect()
    print( "fail out of loop when not operational" )
    print( "display system error message on screen" )
    stall()
    print( "display message: 'press screen or button to end program'" )

# function definitions below
mem_before_function = gc.mem_free()
def calculate_and_write_standard_deviations( UID, batch_number, filename ):
    try:
        with open( filename, "r") as sf:
            sf.readline() #throw away the header line
            line = sf.readline().split(',')
            number_of_samples = int( line[ 2 ] )
            #print( "number of samples == {}".format( number_of_samples ))
            number_of_channels = 12
            sf.close()
            stdev_list = []
            stdev_list=[str(UID), str(batch_number),"standard_deviation"]
            for n in range( 0, number_of_channels ):
                #print( "n == {}".format( n ))
                with open( filename, "r") as sf:
                    dataline=[]
                    sf.readline() #throw away the header line
                    for i in range( 0, number_of_samples ):
                        #print( "i == {}".format( i ))
                        dataline.append( int((sf.readline().split(','))[ n + 3 ]))
                        #print( dataline )
                    #print( dataline )
                    stdev_list.append( "{}".format( ulab.numpy.std( dataline )))
                sf.close()
            print( stdev_list )
            print( "STANDARD DEVIATION SUCCESS!" )

        with open( filename, "a") as sf:
            #sf.write("\n")
            for i,value in enumerate( stdev_list ):
                sf.write( "{},".format( value ))
            sf.close()
        return True
    except (MemoryError, IndexError) as err:
        print( "{}: could not complete standard deviation operation".format(err) )
        time.sleep(2)
        return False

def create_sampling_overlay( display ):
    sampling_group = initialize_display_group( display )
    border_color = 0xFF0022 # red
    block_color = 0x0000FF # blue
    if (display == False) or ( sampling_group == False):
        print("No display")
        return
    block = displayio.Palette(1)
    block[0] = block_color
    rectangle = vectorio.Rectangle(pixel_shader=block, width=320, height=180, x=0, y=30)
    sampling_group.append( rectangle )
    return sampling_group

def create_sampling_text(wait_count, count_down_step_s, display):
    gc.collect()
    count = wait_count
    text_group = displayio.Group( scale=6, x=40, y=60 )
    text = "WAIT"
    text_area = label.Label( terminalio.FONT, text=text, color=0xFFFFFF )
    text_group.append( text_area ) # Subgroup for text scaling
    display.show(text_group)
    while count > 0:
        time.sleep( count_down_step_s)
        count -= 1
        print(count)
        text_area.text = ("WAIT {}".format(count))
    #text_group.scale = 4
    return text_area

def write_tail_of_sampling_file( UID, batch_number, iso8601_utc,
    sample_indicator, sampling_filename, averaging_number_of_samples,
    sample_average, VIS_sensor_temperature, NIR_sensor_temperature, surface_temperature_C,
    air_temperature_C, relative_humidity_percent, range_m
    ):
    gc.collect()
    if sample_indicator:
        sample_indicator.value = ON
    try:
        with open( sampling_filename, "a" ) as sf:
            sf.write( str( UID ))
            sf.write(",")
            sf.write( str( batch_number ))
            sf.write( ", average, " )
            for i,v in enumerate( sample_average ):
                sf.write( str(v) )
                #if i < len(sample_average)-1: # no trailing ','
                sf.write(",")
            #sf.write( "\n" )
            sf.write( " {}, {}, {}, {}, {}, {}\n".format(
                VIS_sensor_temperature, NIR_sensor_temperature, surface_temperature_C,
                air_temperature_C, relative_humidity_percent, range_m, iso8601_utc
                ))
        if sample_indicator:
            sample_indicator.value = OFF
        return True
    except OSError:
        return False

def write_sampling_line_to_file( UID, batch_number, iso8601_utc, sample_indicator, sampling_filename, sampling_index, visible_spectrum_uW_per_cm_squared, nir_spectrum_uW_per_cm_squared ):
    gc.collect()
    with open( sampling_filename, "r" ) as sf:
        sf.flush()
        sf.close()

    if sample_indicator:
        sample_indicator.value = ON
    try:
        with open( sampling_filename, "a" ) as sf:
            sf.write( str( UID ))
            sf.write(",")
            sf.write( str( batch_number ))
            sf.write( ", " )
            sf.write( str( sampling_index ))
            sf.write( ", " )
            for i,v in enumerate(visible_spectrum_uW_per_cm_squared):
                sf.write( str(v) )
                sf.write(",")
            for i,v in enumerate(nir_spectrum_uW_per_cm_squared):
                sf.write( str(v) )
                #if i < len(nir_spectrum_uW_per_cm_squared)-1: # no trailing ','
                sf.write(",")
            sf.write( "0, 0, 0, 0, 0, 0,")
            sf.write( str( iso8601_utc ))
            sf.write( "\n" )
            sf.flush()
            sf.close()
        if sample_indicator:
            sample_indicator.value = OFF
        return True
    except OSError:
        return False

def get_spectral_sensor_temperatures(VIS_sensor, NIR_sensor):
    vis_temp_C = VIS_sensor.temperature

    s = "ATTEMP\n"
    b = bytearray()
    b.extend(s)
    NIR_sensor.write(b)
    #print( "Bytearray sent: %s" % b )
    data = NIR_sensor.readline()
    #print( "Data received: %s" % data)
    datastr = ''.join([chr(b) for b in data]) # convert bytearray to string
    nir_temp_C = datastr.rstrip(" OK\n")
    #print(nir_temp_C)
    return round( vis_temp_C, 1 ), round( int( nir_temp_C ), 1 )

def create_sampling_file( UID, batch_number, timestamp, decimal_hour, values ): # 16 B
    gc.collect()
    sampling_filename = "/sd/{}_{:04}{:02}{:02}_{}.csv".format( UID, timestamp.tm_year, timestamp.tm_mon, timestamp.tm_mday, batch_number)
    try:
        with open( sampling_filename, "w" ) as sf:
            sf.write( "UID, batch_number, sample_number, irradiance_450nm_blue_irradiance_uW_per_cm_squared, irradiance_500nm_cyan_irradiance_uW_per_cm_squared, "
                "irradiance_550nm_green_irradiance_uW_per_cm_squared, irradiance_570nm_yellow_irradiance_uW_per_cm_squared, irradiance_600nm_orange_irradiance_uW_per_cm_squared, "
                "irradiance_650nm_red_irradiance_uW_per_cm_squared, irradiance_610nm_orange_irradiance_uW_per_cm_squared, irradiance_680nm_near_infrared_irradiance_uW_per_cm_squared, "
                "irradiance_730nm_near_infrared_irradiance_uW_per_cm_squared, irradiance_760nm_near_infrared_irradiance_uW_per_cm_squared, "
                "irradiance_810nm_near_infrared_irradiance_uW_per_cm_squared, irradiance_860nm_near_infrared_irradiance_uW_per_cm_squared,  VIS_sensor_temperature_C,  NIR_sensor_temperature_C, "
                "surface_temperature_C, air_temperature_C, relative_humidity_percent, range_m, timestamp\n" )
            sf.close()
        return sampling_filename
    except OSError:
        return False

def write_data_to_file( UID, timestamp, decimal_hour, batch_number, range_m,
                visible_irradiance_uW_per_cm_squared, nir_irradiance_uW_per_cm_squared,
                surface_temperature_C, air_temperature_C, humidity_relative_percent,
                barometric_pressure_hPa,
                main_battery_voltage, sample_indicator ):
    # we let operations fail if the sdcard didn't initialize
    need_header = False
    try:
        os.stat( DATA_FILE ) # fail if data.cav file is not already there
        #raise OSError # uncomment to force header everytime
    except OSError:
        if sample_indicator:
            sample_indicator.value = ON
    try:
        # with sys.stdout as f: # uncomment to write to console (comment "with open")
        with open( DATA_FILE, "a" ) as f: # open seems about 0.016 secs.
            if sample_indicator:
                sample_indicator.value = ON
            iso8601_utc = "{:04}{:02}{:02}T{:02}{:02}{:02}Z".format( timestamp.tm_year,
            timestamp.tm_mon, timestamp.tm_mday, timestamp.tm_hour, timestamp.tm_min,
            timestamp.tm_sec
            )
            decimal_hour = timestamp_to_decimal_hour( timestamp )
            f.write( "{}, {}, {}, {}, ".format( UID, batch_number, iso8601_utc, decimal_hour ))
            f.write( "{}, 1.0, ".format( surface_temperature_C ))
            f.write( "{}, 0.3, ".format( air_temperature_C ))
            f.write( "{}, 1.8, ".format( humidity_relative_percent ))
            f.write( "{}, 1, ".format( barometric_pressure_hPa ))
            f.write( "{}, 0.001, ".format( range_m ))
            for i,band in enumerate( VIS_BANDS ):
                f.write( str(VIS_BANDS[ i ]) )
                f.write( ", ")
                f.write( "5, " )
                f.write( str(visible_irradiance_uW_per_cm_squared[ i ] ))
                f.write( ", ")
                f.write( str(visible_irradiance_uW_per_cm_squared[ i ] * 12/100) )
                f.write( ", ")
            for i,band in enumerate( NIR_BANDS ):
                f.write( str(NIR_BANDS[ i ] ))
                f.write( ", ")
                f.write( "5, " )
                f.write( str(nir_irradiance_uW_per_cm_squared[ i ] ))
                f.write( ", ")
                f.write( str( nir_irradiance_uW_per_cm_squared[ i ] * 12/100 ))
                f.write( ", ")
            f.write( "{}".format( main_battery_voltage ) )
            f.write("\n")
        if sample_indicator:
            sample_indicator.value = OFF
        return True
    except OSError as err:
        # TBD: maybe show something on the display like sd_full? this will "Error" every sample pass
        # "[Errno 30] Read-only filesystem" probably means no sd_card
        print( "Error: sd card fail: {:} ".format(err) )
        if sample_indicator != False:
            sample_indicator.value = ON #  ant ON to show error, likely no SD card present, or SD card full.
        return False
mem_after_function = gc.mem_free()
print("mem used for function load =={} B".format(mem_after_function - mem_before_function))

def screen_pressed( touch_screen ):
    if touch_screen:
        top_center_pressed = False  #blue screen button
        top_left_pressed = False    #yellow screen button
        top_right_pressed = False   #green screen button
        if alt_display_present:
            if touch_screen.touched:
                point = touch_screen.touch
                print( point )
                touch_y = point["x"] #coordinate transform to make it more like the display coordinates
                touch_x = point["y"]
                if touch_y > 2300:
                    if touch_x in range( 540, 1400 ):
                        top_left_pressed = True #yellow
                    elif touch_x in range( 1500, 2600 ):
                        top_center_pressed = True #blue
                    elif touch_x in range( 2700, 3500 ):
                        top_right_pressed = True #green
                return top_left_pressed, top_center_pressed, top_right_pressed
            else:
                return False, False, False
        else:
            while not touch_screen.buffer_empty:
                touch_y, touch_x, touch_pressure = touch_screen.read_data()
                #print( "touch_x, touch_y, touch_pressure" )
                #print( touch_x, touch_y, touch_pressure )
                if touch_y > 2300:
                    if touch_x in range( 2350, 3750 ):
                        top_left_pressed = True
                    elif touch_x in range( 1220, 2300 ):
                        top_center_pressed = True
                    elif touch_x in range( 180, 1150 ):
                        top_right_pressed = True
            return top_left_pressed, top_center_pressed, top_right_pressed
    else:
        return False, False, False

def show_telltale( display_group, instrument_mode ):
    BANISH = 250
    if display_group:
        len_group = ( len ( display_group ))
        if instrument_mode == RECORD_PAUSE:
            display_group[ len_group - 5 ].y = BANISH  #green tab
            display_group[ len_group - 6 ].y = 158  #blue tab
            display_group[ len_group - 7 ].y = BANISH  #yellow tab

        if instrument_mode == AVERAGE:
            display_group[ len_group - 5 ].y = 193  #green tab
            display_group[ len_group - 6 ].y = BANISH  # blue tab
            display_group[ len_group - 7 ].y = BANISH  #yellow tab

        if instrument_mode == SINGLE:
            display_group[ len_group - 5 ].y = BANISH  #green tab
            display_group[ len_group - 6 ].y = BANISH  #blue tab
            display_group[ len_group - 7 ].y = 121  #yellow tab
    else:
        print("No display")
        return

def create_telltale( display_group ):
    telltale_color_x = 161
    telltale_color_width = 16 - 4
    telltale_color_height = 34
    BLUE = 0x00BFFF
    YELLOW = 0xF8FF33
    GREEN = 0x34FF1A
    if display_group:
        yellow_tab = Rect( telltale_color_x, 121, telltale_color_width, telltale_color_height-0, fill = YELLOW)
        display_group.append( yellow_tab )
        blue_tab = Rect( telltale_color_x, 121+34+2+1, telltale_color_width, telltale_color_height-1, fill = BLUE)
        display_group.append( blue_tab )
        green_tab = Rect( telltale_color_x, 125+34*2, telltale_color_width, telltale_color_height, fill = GREEN)
        display_group.append( green_tab )
    else:
        print("No display")
        return

def show_record_pause_icon( display_group, record_pause_state, create_or_update ):
    if display_group == False:
        print("No display")
        return
    #print ( len ( display_group ))
    #WHITE = 0xFFFFFF
    BACKGROUND = 0x99E6FF
    RED = 0xFF0022
    BLACK = 0x000000
    x_center = 170
    y_center = 70
    radius = 8
    x_corner = x_center - radius
    y_corner = y_center - radius
    off_screen_y = 300
    width = 18
    split_width = int( width/3 )
    height = 18
    if create_or_update == 0:
        blank_icon = Rect( x_corner, y_corner, width, height, fill = BACKGROUND )#WHITE)
        recording_icon = Circle( x_center, y_center, radius, fill = RED)
        pause_square_icon = Rect( x_corner, y_corner, width, height, fill = BLACK)
        pause_split_icon = Rect( x_corner + split_width, y_corner, split_width, height, fill = BACKGROUND) #WHITE)
        display_group.append( blank_icon )
        display_group.append( recording_icon )
        display_group.append( pause_square_icon )
        display_group.append( pause_split_icon )
    else:
        len_group = ( len ( display_group ))
        if record_pause_state == 0:
            display_group[ len_group - 4 ].y = y_corner
            display_group[ len_group - 3 ].y = y_corner
            display_group[ len_group - 2 ].y = off_screen_y
            display_group[ len_group - 1 ].y = off_screen_y
        elif record_pause_state == 1:
            display_group[ len_group - 4 ].y = off_screen_y
            display_group[ len_group - 3 ].y = off_screen_y
            display_group[ len_group - 2 ].y = y_corner
            display_group[ len_group - 1 ].y = y_corner

def remove_record_pause_icon ( display_group ):
    len_group = ( len ( display_group ))
    display_group[ len_group - 2 ].y = 250
    display_group[ len_group - 1 ].y = 250

def populate_table_values( text_group, UID, timestamp, decimal_hour, batch_number, visible_irradiance_uW_per_cm_squared,
        nir_irradiance_uW_per_cm_squared, surface_temperature_C, air_temperature_C, range_m, Li_present):
    day_date = "UID:{0} {1:04}-{2:02}-{3:02}".format( UID, timestamp.tm_year, timestamp.tm_mon, timestamp.tm_mday )
    # don't take time to update display if not changed:
    #if battery_voltage > LOW_BATTERY_VOLTAGE:
    gc.collect()
    if text_group[ GROUP.DAY_DATE ].text != day_date:
        text_group[ GROUP.DAY_DATE ].text = day_date
    if text_group[ GROUP.BATCH ].text != batch_number:
        text_group[ GROUP.BATCH ].text = batch_number
    time_text = "{0:02}:{1:02}:{2:02}Z".format(timestamp.tm_hour, timestamp.tm_min, timestamp.tm_sec)
    text_group[ GROUP.TIME ].text = time_text
    text_group[ GROUP.SURFACE_TEMP ].text = "{:4}C".format( surface_temperature_C )
    for i,band in enumerate( VIS_BANDS ):
        waveband_string = "{:5}".format( visible_irradiance_uW_per_cm_squared[ i ] )
        text_group[ GROUP.VIS_VALUES + i ].text = waveband_string
    for i,band in enumerate( NIR_BANDS ):
        waveband_string = "{:4}".format( nir_irradiance_uW_per_cm_squared[ i ] )
        text_group[ GROUP.NIR_VALUES + i ].text = waveband_string
    if Li_present:
        if range_m:
            text_group[ GROUP.AIR_TEMPERATURE ].text = "{:.2}m".format( range_m )
        else:
            pass
            #text_group[ GROUP.AIR_TEMPERATURE ].text = "----"
    else:
        text_group[ GROUP.AIR_TEMPERATURE ].text = "{:}C".format( air_temperature_C )
    gc.collect()

def pushbutton_pressed( pushbutton ):
    pushbutton_press_state = not pushbutton.value   #active low, so True is notpushed and False is pushed
    return pushbutton_press_state                   #pushbutton_press_state is True if button is being pushed

def check_inputs( pushbutton, touch_screen, pushbutton_last_press_state, screen_record_pause_last_press_state, source_lamps_last_press_state ):
    pushbutton_press_state = pushbutton_pressed( pushbutton )
    press = screen_pressed( touch_screen )
    if pushbutton_last_press_state == pushbutton_press_state and screen_record_pause_last_press_state == press[0] and source_lamps_last_press_state == press[1]:
        no_change = True
    else:
        no_change = False
    return no_change, pushbutton_press_state, press[0], press[1]

def mirror_data_over_usb(
    UID, timestamp, decimal_hour, batch_number,
    visible_spectrum_uW_per_cm_squared, nir_spectrum_uW_per_cm_squared,
    surface_temperature_C, air_temperature_C, relative_humidity_percent, range_m
    ):
    if True: #False:
        gc.collect()
        #print( "memory free immediately before mirror data == {} kB.".format( gc.mem_free()/1000))
        print( "mirror_data", end=", " )
        print( "UID, {}".format( UID ), end=", ")
        print( "batch, {}".format( batch_number ), end=", ")
        print( "year, {}".format( timestamp.tm_year ), end=", ")
        print( "month, {}".format( timestamp.tm_mon ), end=", ")
        print( "day, {}".format( timestamp.tm_mday ), end=", ")
        print( "hour, {}".format( timestamp.tm_hour ), end=", ")
        print( "min, {}".format( timestamp.tm_min ), end=", ")
        print( "sec, {}".format( timestamp.tm_sec ), end=", ")
        print( "dec_hour, {}".format( decimal_hour ), end=", ")
        print( "range, {}".format( range_m ), end=", ")
        print( "v450, {}".format( visible_spectrum_uW_per_cm_squared[ 0 ]), end=", ")
        print( "b500, {}".format( visible_spectrum_uW_per_cm_squared[ 1 ]), end=", ")
        print( "g550, {}".format( visible_spectrum_uW_per_cm_squared[ 2 ]), end=", ")
        print( "y570, {}".format( visible_spectrum_uW_per_cm_squared[ 3 ]), end=", ")
        print( "o600, {}".format( visible_spectrum_uW_per_cm_squared[ 4 ]), end=", ")
        print( "r650, {}".format( visible_spectrum_uW_per_cm_squared[ 5 ]), end=", ")
        print( "610, {}".format( nir_spectrum_uW_per_cm_squared[ 0 ]), end=", ")
        print( "680, {}".format( nir_spectrum_uW_per_cm_squared[ 1 ]), end=", ")
        print( "730, {}".format( nir_spectrum_uW_per_cm_squared[ 2 ]), end=", ")
        print( "760, {}".format( nir_spectrum_uW_per_cm_squared[ 3 ]), end=", ")
        print( "810, {}".format( nir_spectrum_uW_per_cm_squared[ 4 ]), end=", ")
        print( "860, {}".format( nir_spectrum_uW_per_cm_squared[ 5 ]), end=", ")
        print( "surface_temp, {}".format( surface_temperature_C ), end=", ")
        print( "air_temp, {}".format( air_temperature_C ), end=", ")
        print( "rel_humidity, {}".format( relative_humidity_percent ))
    return True

def read_LiDAR_sensor( LiDAR_sensor ):
    try:
        start_time = time.monotonic()
        while not LiDAR_sensor.data_ready:
            pass
        stop_time = time.monotonic()
        #print( "elapsed time = {}s".format(stop_time - start_time))
        LiDAR_sensor.clear_interrupt()
        distance_m = ( LiDAR_sensor.distance * 10 / 1000 )
        if distance_m != 0:
            distance_m += 0.007
            return distance_m
        else:
            return False
    except (AttributeError, ValueError) as err:
        print( "read LiDAR sensor failed: {}".format( err ))
        return False

def read_NIR_sensor( NIR_sensor ):
    # returns: R_610nm, S_680nm, T_730nm, U_760nm, V_810nm, W_860nm
    if NIR_sensor == False:
        return [0 for x in NIR_BANDS]
    decimal_places = 1
    b = b"ATCDATA\n"
    NIR_sensor.write(b)
    data = NIR_sensor.readline()
    if data is None:                    #if returned data is of type NoneType
        print( "Alert: Sensor miswired at main instrument board, or not properly modified for uart usage" )
        print( "Enable serial communication via uart by removing solder from the jumpers labeled JP1," )
        print( "and add solder to the jumper labeled JP2 (on the back of the board)" )
        return [0 for x in NIR_BANDS]
    else:
        #print(gc.mem_free())
        datastr = data.decode("utf-8")
        #datastr = "".join([chr(b) for b in data]) # convert bytearray to string
        datastr = datastr.rstrip(" OK\n")
        if datastr == "ERROR":
            print("Error: NIR read")
            return [0 for x in NIR_BANDS]
        # convert to float, and round, in place
        datalist_nir = datastr.split( ", " )
        for n, value in enumerate(datalist_nir): # should be same number as items in NIR_BANDS
            try:
                as_float = float( value )
                value = int( round( as_float, decimal_places ))
                if value > MAX_VALUE:
                    value = MAX_VALUE
            except ValueError:
                print("Failed to convert '{:}' to a float") # only during verbose/development
                value = 0
            # After converting to float, element {:} of the list is: {:0.1f} and is type".format( n, datalist_nir[n] ) )
            datalist_nir[n] = value
    # R_610nm, S_680nm, T_730nm, U_760nm, V_810nm, W_860nm
    if HUGE_VALUE:
        datalist_nir = ( HUGE_VALUE, HUGE_VALUE, HUGE_VALUE, HUGE_VALUE, HUGE_VALUE, HUGE_VALUE)
    return datalist_nir

def read_spectrum( spectral_sensor ):
    gc.collect()
    decimal_places = 1
    # returns readings in a tuple, six channels of data
    # Calibration information from AS7262 datasheet:
    # Each channel is tested with GAIN = 16x, Integration Time (INT_T) = 166ms and VDD = VDD1 = VDD2 = 3.3V, TAMB=25°C.
    # The accuracy of the channel counts/μW/cm2 is ±12%.
    # Sensor Mode 2 is a continuous conversion of light into data on all 6 channels
    # 450nm, 500nm, 550nm, 570nm, 600nm and 650nm
    # sensor.violet returns the calibrated floating point value in the violet channel.
    # sensor.raw_violet returns the uncalibrated decimal count value in the violet channel.
    # that syntax is the same for each of the 6 channels

    # NOTE: the library was written for the visible sensor, as7262.
    # The library works exactly the same for the near infrared sensor, as7263,
    # but the band names are called as if the sensor were the visible one.
    if spectral_sensor:
        try:
            while not spectral_sensor.data_ready:
                time.sleep(0.01)
            initial_read_time_s = time.monotonic()
            violet_calibrated = int( round( spectral_sensor.violet, decimal_places ))
            if violet_calibrated > MAX_VALUE:
                violet_calibrated = MAX_VALUE
            blue_calibrated = int( round( spectral_sensor.blue, decimal_places ))
            if blue_calibrated > MAX_VALUE:
                blue_calibrated = MAX_VALUE
            green_calibrated = int( round( spectral_sensor.green, decimal_places ))
            if green_calibrated > MAX_VALUE:
                green_calibrated = MAX_VALUE
            yellow_calibrated = int( round( spectral_sensor.yellow, decimal_places ))
            if yellow_calibrated > MAX_VALUE:
                yellow_calibrated = MAX_VALUE
            orange_calibrated = int( round( spectral_sensor.orange, decimal_places ))
            if orange_calibrated > MAX_VALUE:
                orange_calibrated = MAX_VALUE
            red_calibrated = int( round( spectral_sensor.red, decimal_places ))
            if red_calibrated > MAX_VALUE:
                red_calibrated = MAX_VALUE
            final_read_time_s = time.monotonic()
            read_time_s = final_read_time_s - initial_read_time_s
            #print( "Sensor read time s = {}".format( read_time_s ))
            spectral_tuple = ( violet_calibrated, blue_calibrated, green_calibrated, yellow_calibrated, orange_calibrated, red_calibrated )
            if HUGE_VALUE:
                spectral_tuple = ( HUGE_VALUE, HUGE_VALUE, HUGE_VALUE, HUGE_VALUE, HUGE_VALUE, HUGE_VALUE)
            return spectral_tuple
        except ValueError as err:
            print( "Error: spectral sensor fail: {:}".format(err) )
            return ( 0, 0, 0, 0, 0, 0)
    else:
       return ( 0, 0, 0, 0, 0, 0)

def read_WX_sensor( WX_sensor ):
    decimal_places = 1
    if WX_sensor:
        try:
            relative_humidity_percent = int(round( WX_sensor.humidity, decimal_places ))
            barometric_pressure_hPa = int(round( WX_sensor.pressure, decimal_places ))
        except ValueError:
            print( "Error: WX sensor failed to read:  {:}".format(err) )
    else:
        relative_humidity_percent = 0
        barometric_pressure_hPa = 1
    return relative_humidity_percent, barometric_pressure_hPa

def read_AT_sensor( AT_sensor ):
    if AT_sensor:
        # mcp9808 datasheet: accuracy +/- 0.25 C
        decimal_places = 1
        try:
            air_temperature_C = round( AT_sensor.temperature, decimal_places )
            return air_temperature_C
        except ValueError as err:
            print( "Error: air temperature sensor fail: {:}".format(err) )
            return -273
    else:
        return -273

def read_TIR_sensor( TIR_sensor ):
    if TIR_sensor:
        decimal_places = 1
        try:
            surface_temperature_C = TIR_sensor.object_temperature
            surface_temperature_C = round( surface_temperature_C, decimal_places )
            return surface_temperature_C
        except ValueError as err:
            print( "Error: thermal infrared sensor fail: {:}".format(err) )
            return -273
    else:
        return -273

def initialize_LiDAR_sensor( i2c_bus ):
    if i2c_bus == False:
        return False
    try:
        vl53 = adafruit_vl53l4cd.VL53L4CD( i2c_bus )
        vl53.start_ranging()
        return vl53
    except (ValueError, OSError, NameError) as err:
        print( "LiDAR sensor not found: {:}".format(err) )
        return False


def initialize_VIS_sensor( i2c_bus ):
    if i2c_bus == False:
        return False
    try:
        VIS_sensor = adafruit_as726x.AS726x_I2C( i2c_bus )
        print( "\nSet visible spectral sensor to 16x gain and 166 ms integration time, because those are the values used at the factory during the sensor calibration." )
        VIS_sensor.conversion_mode = VIS_sensor.MODE_2
        print( "Default Gain = {}".format( VIS_sensor.gain ))
        VIS_sensor.gain = 16
        print( "Gain Now = {}".format( VIS_sensor.gain ))
        print( "Default Integration Time = {} ms".format( VIS_sensor.integration_time ))
        VIS_sensor.integration_time = 166 # from 1 to 714 = 255 * 2.8ms ms
        print( "Integration Time Now = {} ms".format( VIS_sensor.integration_time ))
        return VIS_sensor
    except ValueError as err:
        print( "Error: visible spectrum sensor fail: {:}".format(err) )
        return False

def initialize_NIR_sensor( uart_bus ):
    if uart_bus == False:
        return False
    uart_bus.write(b"AT\n")
    data = uart_bus.readline() #typical first response from the device is b'ERROR\n', which indicates it is present.
    if data is None:
        print ( "Error: near infrared spectrum sensor fail" )
        return False
    else:
        print( "\nSet near infrared spectral sensor to 16x gain and 166 ms integration time, because those are the values used at the factory during the sensor calibration." )
        # initialized near infrared spectrum sensor
        # check gain setting
        b = b"ATGAIN\n"
        uart_bus.write(b)
        data = str( uart_bus.readline() )[2]
        print( "default gain setting = {}, where 0 is 1x gain, 1 is 3.7x gain, 2 is 16x gain, and 3 is 64x gain".format( data ))

        b = b"ATGAIN=2\n"
        uart_bus.write(b)
        data = str( uart_bus.readline() )[2:4]
        print( "gain setting successful == {}".format( data ))

        b = b"ATGAIN\n"
        uart_bus.write(b)
        data = str( uart_bus.readline() )[2]
        print( "updated gain setting = {}, where 0 is 1x gain, 1 is 3.7x gain, 2 is 16x gain, and 3 is 64x gain".format( data ))

        #check integration time setting
        b = b"ATINTTIME=166\n"
        uart_bus.write(b)
        data = uart_bus.readline()

        b = b"ATINTTIME\n"
        uart_bus.write(b)
        data = str( uart_bus.readline() )[2:6]
        print( "updated integration time setting = {}ms".format( data ))
        # print( "# NIR spectrum default INTTIME (59 * 2.8ms = 165ms): {:}".format(data))
        return uart_bus

def initialize_TIR_sensor( i2c_bus ):
    if i2c_bus == False:
        return False
    try:
        TIR_sensor = adafruit_mlx90614.MLX90614( i2c_bus )
        # initialized thermal infrared sensor
        return TIR_sensor
    except ValueError as err:
        #print( "Error: thermal infrared sensor fail: {:}".format(err) )
        return False

def initialize_AT_sensor( i2c_bus ):
    if i2c_bus == False:
        return False
    try:
        AT_sensor = adafruit_mcp9808.MCP9808( i2c_bus )
        # initialized air temperature sensor
        return AT_sensor
    except ValueError as err:
        #print( "Error: air temperature sensor fail: {:}".format(err) )
        return False

def initialize_WX_sensor( i2c_bus ):
    if i2c_bus == False:
        return False
    try:
        WX_sensor = adafruit_bme280.Adafruit_BME280_I2C( i2c_bus )
        # initialized weather sensor
        return WX_sensor
    except RuntimeError as err:
        #print( "Runtime Error: weather sensor fail {:}".format(err) )
        return False
    except ValueError as err:
        #print( "Error: weather sensor fail {:}".format(err) )
        return False

def initialize_pushbutton( IO_pin ):
    pushbutton = digitalio.DigitalInOut( IO_pin )
    pushbutton.direction = digitalio.Direction.INPUT
    pushbutton.pull = digitalio.Pull.UP
    return pushbutton

def initialize_data_file():
    need_header = False
    try:
        os.stat( DATA_FILE ) # fail if data.csv file is not already there
        #raise OSError # uncomment to force header everytime
        return False
    except OSError:
        # setup the header for first time
        need_header = True
        #print( "need header = True")
        if need_header:
            preheader_mem_free = gc.mem_free()
            print( "preheader mem free = {}".format(preheader_mem_free))
            gc.collect()
            try:
                with open( DATA_FILE, "w" ) as f:
                    header = (
                    "UID, batch_number, timestamp, decimal_hour, "
                    "surface_temperature_C, surface_temperature_uncertainty_C, "
                    "air_temperature_C, air_temperature_uncertainty_C, "
                    "relative_humidity_percent, relative_humidity_uncertainty_percent, "
                    "barometric_pressure_hPa, barometric_pressure_uncertainty_hPa, "
                    "range_m, range_uncertainty_m, "
                    "irradiance_450nm_blue_wavelength_nm, irradiance_450nm_blue_wavelength_uncertainty_nm, irradiance_450nm_blue_irradiance_uW_per_cm_squared, irradiance_450nm_blue_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_500nm_cyan_wavelength_nm, irradiance_500nm_cyan_wavelength_uncertainty_nm, irradiance_500nm_cyan_irradiance_uW_per_cm_squared, irradiance_500nm_cyan_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_550nm_green_wavelength_nm, irradiance_550nm_green_wavelength_uncertainty_nm, irradiance_550nm_green_irradiance_uW_per_cm_squared, irradiance_550nm_green_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_570nm_yellow_wavelength_nm, irradiance_570nm_yellow_wavelength_uncertainty_nm, irradiance_570nm_yellow_irradiance_uW_per_cm_squared, irradiance_570nm_yellow_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_600nm_orange_wavelength_nm, irradiance_600nm_orange_wavelength_uncertainty_nm, irradiance_600nm_orange_irradiance_uW_per_cm_squared, irradiance_600nm_orange_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_650nm_red_wavelength_nm, irradiance_650nm_red_wavelength_uncertainty_nm, irradiance_650nm_red_irradiance_uW_per_cm_squared, irradiance_650nm_red_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_610nm_orange_wavelength_nm, irradiance_610nm_orange_wavelength_uncertainty_nm, irradiance_610nm_orange_irradiance_uW_per_cm_squared, irradiance_610nm_orange_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_680nm_near_infrared_wavelength_nm, irradiance_680nm_near_infrared_wavelength_uncertainty_nm, irradiance_680nm_near_infrared_irradiance_uW_per_cm_squared, irradiance_680nm_near_infrared_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_730nm_near_infrared_wavelength_nm, irradiance_730nm_near_infrared_wavelength_uncertainty_nm, irradiance_730nm_near_infrared_irradiance_uW_per_cm_squared, irradiance_730nm_near_infrared_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_760nm_near_infrared_wavelength_nm, irradiance_760nm_near_infrared_wavelength_uncertainty_nm, irradiance_760nm_near_infrared_irradiance_uW_per_cm_squared, irradiance_760nm_near_infrared_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_810nm_near_infrared_wavelength_nm, irradiance_810nm_near_infrared_wavelength_uncertainty_nm, irradiance_810nm_near_infrared_irradiance_uW_per_cm_squared, irradiance_810nm_near_infrared_irradiance_uncertainty_uW_per_cm_squared, "
                    "irradiance_860nm_near_infrared_wavelength_nm, irradiance_860nm_near_infrared_wavelength_uncertainty_nm, irradiance_860nm_near_infrared_irradiance_uW_per_cm_squared, irradiance_860nm_near_infrared_irradiance_uncertainty_uW_per_cm_squared, "
                    "battery_voltage"
                    "\n"
                        )

                    f.write( header )
            except OSError:
                pass
            postheader_mem_free = gc.mem_free()
            print( "header mem usage = {}".format(postheader_mem_free - preheader_mem_free))
        return True

def check_STELLA_config():
    try:
        with open( "/sd/STELLA_config.txt", "r" ) as s:
            try:
                header = s.readline()
                #print( sampling_header )
                values = s.readline()
                #print(sampling_values)
                values= values.rstrip('\n')
                values = values.split(',')
                #print(line)
                desired_sample_interval_s = float( values[ 0 ])
                averaging_number_of_samples = int( values[ 1 ])
                averaging_count_down = int( values[ 2 ])
                count_down_step_s = float( values[ 3 ])

            # TBD: catch error when /sd doesn't exist
            except ValueError:
                pass
    except OSError:
            print( "sampling_config.txt file not found" )
            try:
                with open( "/sd/STELLA_config.txt", "w" ) as s:
                    s.write( "desired_sample_interval_s, averaging_number_of_samples, averaging_count_down, count_down_step_s\n" )
                    s.write( "0.75, 20, 3, 0.75\n" )
                print("created new STELLA_config.txt file")
            except OSError:
                pass
            desired_sample_interval_s, averaging_number_of_samples, averaging_count_down, count_down_step_s = 0.75, 20, 3, 0.75
    return desired_sample_interval_s, averaging_number_of_samples, averaging_count_down, count_down_step_s

def initialize_sdcard( spi_bus ):
    gc.collect()
    if spi_bus == False:
        return False # will fail later if we try use the directory /sd
    # NOTE: pin D10 is in use by the display for SPI D/C, not available for the SD card chip select.
    # Modify the Adalogger Featherwing to use pin D11 for SD card chip select
    sd_cs = board.D11
    try:
        sdcard = sdcardio.SDCard( spi_bus, sd_cs )
        vfs = storage.VfsFat( sdcard )
        storage.mount( vfs, "/sd" )
        # initialized sd card
        return sdcard
    except ValueError as err: # known error behavior
        print( "Error: sd-card init fail: {:}".format(err) )
        return False
    except OSError as err:
        #TBD distinguish between '[Errno 17] File exists' (already mounted), no card and full card conditions.
        # "card not present or card full: [Errno 5] Input/output error" retry?
        print( "Error: sd card fail: card not present or card full: {:} ".format(err) )
        return False

def update_batch( timestamp ):
    gc.collect()
    datestamp = "{:04}{:02}{:02}".format( timestamp.tm_year, timestamp.tm_mon, timestamp.tm_mday)
    try:
        with open( "/sd/batch.txt", "r" ) as b:
            try:
                previous_batchfile_string = b.readline()
                previous_datestamp = previous_batchfile_string[ 0:8 ]
                previous_batch_number = int( previous_batchfile_string[ 8: ])
            # TBD: catch error when /sd doesn't exist
            except ValueError:
                previous_batch_number = 0
                # corrupted data in batch number file, setting batch to 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 )
        # TBD: catch error when /sd doesn't exist
    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 initialize_real_time_clock( i2c_bus ):
    try:
        hardware_clock = adafruit_pcf8523.PCF8523( i2c_bus )
        clock_battery_OK = not hardware_clock.battery_low
        if clock_battery_OK:
            print( "clock battery is OK." )
        else:
            print( "clock battery is low. Replace the clock battery." )
    except NameError:
        print( "real time clock failed to initialize" )
        hardware_clock = False
    return ( hardware_clock, clock_battery_OK )

def create_welcome_screen( display ):
    welcome_start_memory = gc.mem_free()
    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=25, y=210 )
        text = "software version {}".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 )
        # Add the Group to the Display
        #display.show(group)
    except (MemoryError, OSError):
        try:
            print( "bitmap image file not found or memory not available" )
            welcome_group = initialize_display_group( display )
            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=6, x=50, y=110 )
            text = "STELLA"
            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 )
        except (MemoryError, OSError):
            wc_length = len( welcome_group )
            for x in range (0, wc_length):
                welcome_group.pop()
                print("pop welcome group" )
    welcome_stop_memory = gc.mem_free()
    print( "welcome routine uses {} kB".format( -1 *(welcome_stop_memory - welcome_start_memory)/1000))
        #uses 2.4kB"
    return welcome_group

def full_spectrum_frame( table_group, border_color ):
    # begin full spectrum frame
    if table_group == False:
        return
        #Make the background
    palette = displayio.Palette(1)
    palette[0] = border_color
    background_rectangle = vectorio.Rectangle(pixel_shader=palette, width=SCREENSIZE_X, height=SCREENSIZE_Y, x=0, y=0)
    table_group.append( background_rectangle )
    palette = displayio.Palette(1)
    palette[0] = 0xFFFFFF
    border_width = 7 #0 #7

    #Make the foreground_rectangle
    foreground_rectangle = vectorio.Rectangle(pixel_shader=palette, width=SCREENSIZE_X - 2*border_width, height=SCREENSIZE_Y - 2*border_width, x=border_width, y=border_width)
    table_group.append( foreground_rectangle )

    #Draw the record_pause_state circle:
    palette = displayio.Palette(1)
    palette[0] = 0x99E6FF #0x00BFFF
    record_pause_circle = vectorio.Circle( pixel_shader=palette, radius=45, x=170, y=70 )
    table_group.append( record_pause_circle )

    #Draw the single_point circle:
    palette = displayio.Palette(1)
    palette[0] = 0xFCFF99 #0xF8FF33
    single_point_circle = vectorio.Circle( pixel_shader=palette, radius=45, x=65, y=70 )
    table_group.append( single_point_circle )

    #Draw the sample_and_average_circle:
    palette = displayio.Palette(1)
    palette[0] = 0x9CFF8F #0x34FF1A
    sample_and_average_circle = vectorio.Circle( pixel_shader=palette, radius=45, x=270, y=70 )
    table_group.append( sample_and_average_circle )

    #Make the frame
    batch_x = 258
    batch_border_offset = 0
    batch_height_y = 26
    palette = displayio.Palette(1)
    palette[0] = border_color
    batch_border = vectorio.Rectangle(pixel_shader=palette, width=SCREENSIZE_X - batch_x - border_width - batch_border_offset, height=batch_height_y, x=batch_x, y=border_width + batch_border_offset)
    table_group.append( batch_border )

    batch_area_border_width = 3
    palette = displayio.Palette(1)
    palette[0] = 0xFFFFFF
    batch_clear_area = vectorio.Rectangle(pixel_shader=palette, width=SCREENSIZE_X - batch_x - border_width - batch_area_border_width, height=batch_height_y - batch_area_border_width, x=batch_x + batch_area_border_width, y=border_width)
    table_group.append( batch_clear_area )

    #print( "draw a black narrow vertical rectangle" )
    palette = displayio.Palette(1)
    palette[0] = 0x000000
    telltale_background = vectorio.Rectangle( pixel_shader=palette, width=16, height=110, x=160, y=120 )
    table_group.append( telltale_background )

    #print( "draw a white narrower rectangle to make a black rectangular frame" )
    telltale_frame_width = 2
    palette = displayio.Palette(1)
    palette[0] = 0xFFFFFF
    telltale_background = vectorio.Rectangle( pixel_shader=palette, width=16 - 2 * telltale_frame_width , height=110 - 2 * telltale_frame_width, x=160 + telltale_frame_width, y=120+telltale_frame_width )
    table_group.append( telltale_background )

    #print( "draw a black narrow separators" )
    palette = displayio.Palette(1)
    palette[0] = 0x000000
    telltale_separator = vectorio.Rectangle( pixel_shader=palette, width=16, height=2, x=160, y=120+34+2+1 )
    table_group.append( telltale_separator )

    #print( "draw a black narrow separators" )
    palette = displayio.Palette(1)
    palette[0] = 0x000000
    telltale_separator2 = vectorio.Rectangle( pixel_shader=palette, width=16, height=2, x=160, y=120+2*34+2+1+1 )
    table_group.append( telltale_separator2 )


def create_table_screen( display, display_group ):
    RED = 0xFF0022
    full_spectrum_frame( display_group, RED )
    text_group = full_spectrum_text_groups( display_group )
    return text_group

def full_spectrum_text_groups( table_group ):
    if table_group == False:
        return False
    # Fixed width font
    fontPixelWidth, fontPixelHeight = terminalio.FONT.get_bounding_box()
    text_color = 0x000000 # black text for readability
    text_group = displayio.Group( scale = 2, x = 15, y = 20 ) #scale sets the text scale in pixels per pixel
    try:
        # Name each text_group with some: GROUP.X = len(text_grup), then use in text_group[ GROUP.X ]
        # Order doesn't matter for that (but is easier to figure out if in "display order")
        # LINE 1
        GROUP.DAY_DATE = len(text_group) # text_group[ i ] day date
        text_area = label.Label( terminalio.FONT, color = text_color ) #text color
        text_area.y = -1
        text_area.x = 0
        text_group.append( text_area )
        GROUP.BATCH = len(text_group) #text_group[ i ] batch_display_string
        text_area = label.Label( terminalio.FONT, color = text_color )
        text_area.y = -2
        text_area.x = 127
        text_group.append( text_area )
        # LINE 2
        GROUP.TIME = len(text_group) #text_group[ i ] time
        text_area = label.Label( terminalio.FONT, color = text_color )
        text_area.y = 12
        text_area.x = 0
        text_group.append( text_area )
        # surface temperature label, doesn't need a name
        text_area = label.Label( terminalio.FONT, text="Surface:", color = text_color )
        text_area.y = 12
        text_area.x = 70
        text_group.append( text_area )
        GROUP.SURFACE_TEMP = len(text_group) #text_group[ i ] surface temperature
        text_area = label.Label( terminalio.FONT, color = text_color )
        text_area.y = text_group[-1].y
        text_area.x = text_group[-1].x + len( text_group[-1].text ) * fontPixelWidth # use the previous text to get offset
        text_group.append( text_area )
        # LINE 3
        # units_string, doesn't need a name
        text_area = label.Label( terminalio.FONT, text="nm: uW/cm^2", color = text_color )
        text_area.y = 24
        text_area.x = 0
        text_group.append( text_area )

        # air temp label, doesn't need a name
        text_area = label.Label( terminalio.FONT, text="Air:", color = text_color )
        text_area.x = 94
        text_area.y = 24
        text_group.append( text_area )
        GROUP.AIR_TEMPERATURE = len(text_group) #text_group[ i ] air temperature
        text_area = label.Label( terminalio.FONT, color = text_color )
        text_area.y = text_group[-1].y
        text_area.x = text_group[-1].x + len(text_group[-1].text) * fontPixelWidth
        text_group.append( text_area )
        # LINE 5..10
        #text groups[ i..+5 ] VIS channels labels
        vis_start_x = 0
        for waveband_index,nm in enumerate(VIS_BANDS):
            vis_start_y = 36 + 12 * waveband_index
            # just labels
            label_string = "{:1}{:03}: ".format( VIS_BAND_PREFIXES[waveband_index], nm )
            text_area = label.Label( terminalio.FONT, text=label_string, color = text_color )
            text_area.x = vis_start_x
            text_area.y = vis_start_y
            text_group.append( text_area )
        GROUP.VIS_VALUES = len(text_group) #text groups[ i..+5 ] VIS channels. Just the first one, we `+ i` it
        # x is always the same: a column
        vis_start_x = vis_start_x + len( label_string ) * fontPixelWidth
        for waveband_index,nm in enumerate(VIS_BANDS):
            vis_start_y = 36 + 12 * waveband_index
            text_area = label.Label( terminalio.FONT, color = text_color )
            text_area.x = vis_start_x
            text_area.y = vis_start_y
            text_group.append( text_area )
        #text groups[ i..+5 ] NIR channels labels
        nir_start_x = 82
        for waveband_index,nm in enumerate(NIR_BANDS):
            nir_start_y = 36 + 12 * waveband_index
            # just labels
            label_string = "{:03}: ".format( nm )
            text_area = label.Label( terminalio.FONT, text=label_string, color = text_color )
            text_area.x = nir_start_x
            text_area.y = nir_start_y
            text_group.append( text_area )
        GROUP.NIR_VALUES = len(text_group) #text groups[ i..+5 ] NIR channels. Just the first one, we `+ i` it
        # x is always the same: a column
        nir_start_x = nir_start_x + len( label_string ) * fontPixelWidth
        for waveband_index,nm in enumerate(NIR_BANDS):
            nir_start_y = 36 + 12 * waveband_index
            text_area = label.Label( terminalio.FONT, color = text_color )
            text_area.x = nir_start_x
            text_area.y = nir_start_y
            text_group.append( text_area )
    except RuntimeError as err:
        if str(err) == "Group full":
            print("### Had this many groups when code failed: {:}".format(len(text_group)))
        raise
    table_group.append( text_group )
    #print("TG max_size {:}".format(len(text_group))) # to figure max_size of group
    return text_group

class GROUP:
    '''
    class GROUP:
      a group to gather the index names

      GROUP.X = 1
      magically creates the class variable X in GROUP,
      so we don't have to explicitly declare it

      = len(text_group)
      how many text_groups already made, == index of this text_group

      something = GROUP.X
      Then you just use it. But, this ensures that you assigned to GROUP.X before you read from it.
    '''
    pass

def initialize_touch_screen( spi_bus ):
    touch_screen_chip_select = digitalio.DigitalInOut(board.D6)
    try:
        touch_screen = Adafruit_STMPE610_SPI(spi_bus, touch_screen_chip_select)
        return touch_screen
    except ( RuntimeError, ValueError, NameError) as err:
        print( "STMP610 touch screen controller not found" )
        return False

def initialize_alt_touch_screen( i2c_bus ):
    try:
        touch_screen = adafruit_tsc2007.TSC2007( i2c_bus )
        return touch_screen
    except ( RuntimeError, ValueError, NameError) as err:
        print( "tsc2007 touch screen controller not found" )
        return False

def initialize_display( spi_bus ):
    if spi_bus == False:
        return False
    try:
        # displayio/dafruit_ili9341 library owns the pins until display release
        displayio.release_displays()
        tft_display_cs = board.D9
        tft_display_dc = board.D10
        display_bus = displayio.FourWire( spi_bus, command=tft_display_dc, chip_select=tft_display_cs )
        display = adafruit_ili9341.ILI9341( display_bus, width=SCREENSIZE_X, height=SCREENSIZE_Y, rotation=180 )
        return display
    except ValueError as err:
        print("Error: display fail {:}".format(err))
        return False

def initialize_display_group( display ):
    if display == False:
        print("no display")
        return False
    display_group = displayio.Group()
    return display_group

def low_battery_voltage_notification( text_group ):
    text_group[ GROUP.DAY_DATE ].text = "Low battery: plug in"
    time.sleep(0.6)

def check_battery_voltage( battery_monitor ):
    if battery_monitor:
        battery_voltage = round(( battery_monitor.value * 3.3) / 65536 * 2, 2 )
        return battery_voltage
    else:
        return 10

def initialize_AnalogIn( pin_number ):
    try:
        analog_signal = AnalogIn( pin_number )
    except ValueError:
        print( "analog input initialization failed." )
        analog_signal = False
    return analog_signal

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

def initialize_sample_indicator( IO_pin ):
    try:
        sample_indicator_LED = digitalio.DigitalInOut( IO_pin )
        sample_indicator_LED.direction = digitalio.Direction.OUTPUT
        sample_indicator_LED.value = True #active low, True is off
        return sample_indicator_LED
    except Exception as err:
        print( "Error: led pin initialization failed {:}".format(err) )
        return False

def initialize_discrete_LED( pin_number ):
    try:
        discrete_LED = digitalio.DigitalInOut( pin_number )
        discrete_LED.direction = digitalio.Direction.OUTPUT
    except ValueError:
        print( "discrete LED initialization failed." )
        discrete_LED = False
    return discrete_LED

def initialize_uart_bus( TX_pin, RX_pin ):
    try:
        uart_bus = busio.UART( TX_pin, RX_pin, baudrate=115200, bits=8, parity=1, stop=1)
        return uart_bus
    except ValueError as err: # known error behavior
        print( "Error: uart bus fail: {:}".format(err) )
        return False

def initialize_spi_bus():
    try:
        spi_bus = board.SPI()
        return spi_bus
    except ValueError as err:
        print( "Error: spi bus fail: {:}".format(err) )
        return False

def initialize_i2c_bus( SCL_pin, SDA_pin ):
    try:
        i2c_bus = busio.I2C( SCL_pin, SDA_pin )
    except ValueError as err:
        print( "i2c bus failed to initialize '{}'".format( err ))
        i2c_bus = False
    return i2c_bus

gc.collect()
print( "memory free after load function definitions == {} kB".format( gc.mem_free()/1000))

main()

