# Pymaker 2.1.7
#
# 2b change RI battery from 0.1 ohm to 0.01
# 3b disable LORA (trouble shoot IBat rise), MQTT  loopCounter
# 4b re_enable LORA, stop checking the radios all the time,  how to see 'status' packet ??
# 5b disable FRAM write access but during config,
#    correct scanLTE(), wont crash if none found + disable LTE until next Boot
#    save VOC baseline
# 6b what happens when battV is low ? Ok !
#    how the system recover from going to sleep because of low battery ? Ok !
#    what the cuurent used when bat < 3V ?  I rise to 500mA  VBat drop to 0   SEE 7b
#    LTE and Wifi initialization to MQTT - ok
# 7b go to sleep as soon as detecting Vbat < 3.2V ...
#    COMPATIBLE PYCOMSOCKET VB ONLY !!!
# 8b pycom_socket_version = 3   PyTrackX2 (GPS)
#    deviceSettings['pycom_socket_version']  == 3   PyTrack
#    IMPORTANT !!!
#          PyTrackX2 (GPS)  604565286017  Mouser $39.20
#          for best data use an active antenna part# 604565430465 Mouser  $12.45
#          Jump connctor P3 of Pytrack board
#          Pyport: 6 pins IDC Cables Ribbon Cables .050" FFSD-03-D-03.94-01  Mouser $9.31
#                  https://www.samtec.com/products/ffsd
#
# 9b GPS management
#
# WARNING !!!
# LTE antenna Eval 703 MHz to 803 MHz, 2.5 GHz to 2.6 GHz (LTE Antenna Kit Pycom SWR= ?? )  (ANLTE2-10HRA $6.45  915Mhz SWR=4.8  )
# Lora antenna Eval 915Mhz ( 700461242598 $12.60  SWR=1.4 - good ) (A9D3S $4.99  SWR=4.6 - not well tuned !  )
#
# bug report: "4b Debug1 MQTT.txt"   wlan.isconnected() = True    but network is down !!!
# how much current is available on Pyport 3V3AUX_ SENSE  ?
#
# 2.2.0b use Pytrack instead of 'expansion 3 + socket Vb'
#        give 500ms for mqtt to 'digest' packet ....  Change Quality Of Service from 0 to 1
#             QOS 0 – Once (not guaranteed)
#             QOS 1 – At Least Once (guaranteed)
#             QOS 2 – Only Once (guaranteed)
#        adjust sampling frequency to 1sec cycle
#        Weather shield Va- sensors
#              weather_shield_option = 159 $9F  IR_CO2, ir_c1, PID, H2S, Wind, weather shield PCB
#              b0 = IR_CO2 U5  IC1 $4B  AIN_2P-3N   # U2  IC3 $49  AIN_0        # Vb  IR_CO2 U8  IC1 $4B  AIN_0P-1N
#              b1 = ir_c1 U2  IC3 $49  AIN_0   # U5  IC1 $4B  AIN_2P-3N
#              b2 = PID U1     IC3 $49  AIN_2P-3N
#              b3 = H2S U7     IC4 $4A  AIN_0P-1N
#              b4 = Wind speed IC6 $20
#                   Wind dir   IC3 $49 AIN_1
#              b5 = O3         IC1 $4B   AIN_0Gas-1Ref   2.2.24b
#              b6 = dB         U6   IC4  AIN_2          2.2.24b
#              b7 = weather shield PCB
#              b8 = RESP (SPEC) U6   IC4  AIN_2   !!!db sensor must be disconnected!!!   '+256'  2.3.1
#              b9 = 6814 (NH3)  2.3.2
#              b10 = SO2  2.3.3
#
#              weather_shield_option = 128  $80   standard sensors, weather shield PCB
#              b7 = weather shield PCB
#        Power usage:  Including all std sensors => 320mA (Std NDIR instead of low power)
#
# 2.2.1b Fuel gauge MAX17048
#        Save tvoc baselines at 10min interval
#        Ajust sleep between sampling for 'constant' sampling period of 1sec
#        compute SPEC gain factor,   MFactor_SPEC (sensitivity, TIA)
#           ... you must then update the value in VRAM  ....   configurator.writeVRAM('h2s_gain', MFactor_SPEC)
#        PM sensor must be the last one acquired 'sampleSensors() because of 3.3V disturbance it creates
#        firmware version 1.20.2.r4
#        pybytes version  1.6.1
#        disbale pybytes  =>  import pycom    pycom.pybytes_on_boot(False)
#
# 2.2.2b Receive MQTT messages
#        within boot.py set Skyhook = True (line #17) to compile code for Skyhook
#        skyhook requires FiPy
#        interpret MQTT messages
#
# 2.2.3b Correct error with power mode PM (Skyhook mode)
#        Add ADC chip reference - I2C discovery
#           Pytrack socket VA:
#               Measure power requirement 3VO ... 0.1 Ohm = 0.03  => 300mA/2.92V (2xIR no PID)   200mA/3V with PM OFF
#               IR measurement jump >300ppm between PM ON/OFF cycles (for 100mV variation on power supply)
#         Prevent crash in case of fuel gauge disconnect
#
# 2.2.4b Err light_blinks interval
#        Added carrier network in deviceSettings
#        Disbable qos quality of service  qos = 0 instead of = 1   -  Requested by Eri Apr 8, 2021
#           create variable MQTTqos = 0
#        Add test functions for weather shield PCB: tstWind(), testWeatherShieldAD()
#        Added LTE  APN  teal = standard   // https://docs.pycom.io/tutorials/networks/lte/
#
# 2.2.5b LTE version ?   # import sqnsupgrade     # sqnsupgrade.info()
#        manage the different versions for the battery readings
#        pycom_socket_version = 1     pycom socket VA (skyhook)
#                  ADS1115 AIn2 = VBatt       AIn3 = IBatt      RSense = 0.1
#        pycom_socket_version = 2    pycom socket VB
#                  ADS1115 AIn1 = VBatt       AIN_0 = IBatt      RSense = 0.1
#                  IR enabled
#        pycom_socket_version = 20    pycom socket VC2 with ADS
#                  ADS1115 AIn1 = VBatt       AIN_0 = IBatt      RSense = 0.01
#                  IR enabled
#        pycom_socket_version = 21    pycom socket VC2
#                  Fuelgauge
#        pycom_socket_version = 3     pytrack V2 - pytrack VC2
#                  ADS1115 AIn1 = VBatt       AIN_0 = IBatt      RSense = 0.01
#                  Fuelgauge
#        pycom_socket_version = 4    pycom base VA    2.3.12
#
# 2.2.6b stop LAT LONG search if GPS not ready
#        with all version of PycomSocket but #3 (Pytrack), sync IR reading with PM reading
#
# 2.2.7b correct error IBatt formating
# 2.2.8b correct PM sleep mode with socket version # 20
# 2.2.9b catch errors and store then unto a file - SD card must be installed     file_errLog()
#
# 2.2.10b store the data onto a file ...   file_smpLog()
#         pycom ECONNABORTED  -   https://forum.pycom.io/topic/6310/fipy-lte-not-initialized/5
# 2.2.11b correct gain IR sensor default gain response
# test access sample file ... check access while acq is running
#   enable wifi, run FileZilla, FTP log micro pyhton - It works !!
#   C:\Users\Admin\Documents\Pycom\WebAirMonitor3
# test access sample file ... errase file while acq running - It works !
# 2.2.12b pycom_socket_version = 21    pycom socket VC2
#         Calibrate CO2 and CH4 IR sensor   calibCO2zero()    calibC1zero()
# 2.2.13b Dismiss failure to acquire battery vital - rqt Eric 5/16/2021
# 2.2.15b - mdf 6/16/21
# - imporved hardware cli setup
# - increased connection timeouts for standard (teal) lte network
# - added automatic certificate generation and aws thing registration
# - added default settings for basic questions
# - fix for cli restore previous boolean
# - added sd card freespace to config message
#
# 2.2.16b  do not restore VOC baseline at boot
# - Weather shield:
# - abscence of 3VO will freeze the Fipy as soon as the I2Cswitch is ON and I2C is scanned
#   restore Vbat check at boot (revert 2.2.13b)
# - disconnecting 3VO after boot create an exeption error - checked
# - disconnecting SCL after boot create an exeption error - checked
# - disconnecting SDA after boot create an exeption error checked
# - disconnecting grove sensors after boot create an exeption error - checked
#
# 2.2.17b  interface sample smpcatcher
#          synchronize MQTT message handler with main SampleLoop

# 2.2.17
# I2C map
# $18  MCP9808 skyhook - main
# $19  LIS3DH skyhook
# $1A  MCP9808 skyhook - main
# $21  MCP23017 skyhook - main
# 54=$36  MAX17048  - boot
# 56=$38  PCF8574 - main
# 57=$39  PCF8574 - Pycom base VA  -   Pycom USB V2  - 2.2.29
# 64=$40  HM3300 / HM3300 skyhook - main
# 65=$41  PCA9536  - boot
# $48  ADS1115 / INA219 skyhook - main
# 72=$48 MPSBridge sensor  2.4.14
# 73=$49  ADS1115 / INA219 skyhook - main
# 74=$4A  ADS1115 / INA219 skyhook - main
# 75=$4B  ADS1115 / INA219 skyhook - main
# $4C  INA219 skyhook - main
# $4D  INA219 skyhook - main
# $4E  INA219 skyhook - main
# $4F  INA219 skyhook - main
# $5E  PCA9955B smpcatcher
# $50  M24C02 - EEPROM smpctacher ch#0
# $51  M24C02 - EEPROM smpctacher ch#1
# $52  M24C02 - EEPROM smpctacher ch#2
# $53  M24C02 - EEPROM smpctacher ch#3
# $54  M24C02 - EEPROM smpctacher ch#4
# $55  M24C02 - EEPROM smpctacher ch#5
# $56  M24C02 - EEPROM smpctacher ch#6
# $57  M24C02 - EEPROM smpctacher ch#7
# 88=$58  SGP30 - main
# $60 ($70 at reset) PCA9955B smpcatcher
# $61 ($76 at reset) PCA9955B smpcatcher
# 104=$68  DS1307 - RTC    // 2.2.27
# 106=$69 MCP3424 - ADC 18b 4ch  - Mics6814   // 2.2.29
# 112=$70  PI4M / PCA9955B at reset - boot
# 118=$76  BME280 / PCA9955B at reset - main
# $77  BMP280 - skyhook

# 2.2.18b  disable scan i2c function
#          add IamAlive() to work with DogRelay board
#          share access i2c (no more multiple definition)
#          read or write access to a chip not found lock up i2c ...
#          LTE update ...
#          Fetchtime ...rtc.ntp_sync("time-a-b.nist.gov")
# 2.2.19
# 2.2.20   DogRelay mangement ported to lte, wifi, GPS
#          Err log ported to lte, wifi, GPS
#          rerout machine.reset to handle WeatherShield I2c switch requirements
#
# 2.2.21   Catch error in SGP30
#          PM sleep ctrl is via pin, not port
#          detect BME280 reset and reinit the chip
# 2.2.22b  do definition smpcather ii2 prior comm with wsCommSw
# 2.2.23b  for compatibility with WS VC3
#          re-init BME sensor if detect an error in data
#          LowPower IR are noisy ...
#              add variable co2_range to interface with low noise STD IR sensor 30% STD (instead LowPower 5%)
#              default value for co2_range = 300000   => 30%
# 2.2.24b  add interbal variables  c1_range, co2_range
#          add O3 sensor U4 - configurator.writeFlashKey('weather_shield_option', 159 + 0b0100000 ) # b5=O3
#          add Decibel sensor "U6" - configurator.writeFlashKey('weather_shield_option', 159 + 0b01000000) # b6=dB
#          update 'weather_shield_option' # b5=O3  b6=dB
#
# DBUG
# send ir values in volt. sync IR reading with PM ... not enough PM creates ir reset condition ...
# PM restart may also corrupt BME sensor ?
# swap PM with modified PM (added cap, rerouted power) ...
#
# if err i2c slow down baurate from 100k to 10k ...
#
#
#
# 7b execute booy.py prior main.py
#
# 2.2.24b  Added individual sample queing if unable to send
#
# 2.2.25b Added UI for setup and access point
# 2.2.26
# 2.2.27  RTC DS1307 hardware added
# 2.2.28  RTC DS1307 replaced by RTC PCF8523
# 2.3.0 OTA Updater, remote config and console listener
# 2.3.1 RESP sensor from spec
#       Detect 'network rqt' switch position - Pycom USB V2  - 57=$39  PCF8574
# 2.3.2 6814 sensor for the detection of NH3  ( RED, OX, NH3_ammonia)
# 2.3.3 SO2 sensor from spec
# 2.3.4 SPEC socket select support
#       U4= IC1_ch0= 0  //   U3= IC1_ch2= 1  //  U7= IC4_ch0= 2  //   U6= IC4_ch2= 3
# 2.3.6 Move FRAM variables to JSON file on Flash
#       If you need to add or edit a variable, edit FRAM.json
# 2.3.12 Disable VOC for humidity compensation
#        Disbale VOC baseline correction
#        pycom_socket_version = 4     pycom base VA
# 2.4.14 MPSBridge

# 8b

import machine
wdt = machine.WDT(timeout=2*60*1000) # 2min to complete imports
IamAlive() # 2.2.20


file_errLog(0, "\n\nBooting up ... ", sdAvailable)
from utils import asciiArt
print(asciiArt.clearRepl + "Booting up...", end='')
import os, pycom, time, json, builtins
print('.', end='')
from machine import WDT, RTC
print('.', end='')
# 2.2.0b from sensors import *
# 2.2.0b print('.', end='')
MQTTqos = 0 # 2.2.4b
from networks import *
print('.', end='')
from networks.robustMqtt import MQTTClient
print('.', end='')
from network import LoRa
print('.', end='')
import _thread
print('.', end='')
import configurator
print('.', end='')
import socket
print('.', end='')
import binascii, re, struct
print('.', end='')
from webserver import *
print('.', end='')
import uos  # 6b
from sensors import *  # 2.2.0b
print('.', end='')
import requests

from machine import Timer  # 2.2.0b
chrono = Timer.Chrono() # 2.2.0b

rtc = RTC()
import json # 2.2.2b

import gc
gc.enable() # 6b  garbage collector enabled

import sys # 2.2.9b

version = VERSION
build_date = BUILD_DATE
currentPingTime = -1
currentPacketLoss = 100

from utils import ota

IamAlive() # 2.2.20
wdt.init(60*1000) # 2.2.20      wdt = WDT(timeout=60*1000)  # 60sec

eID = None

rgbStrip.setState("on") # whyte
print("\n\n" + asciiArt.boldHR + asciiArt.parrotArt + "\nv{}\t\t\t{}".format(version,build_date) + asciiArt.boldHR)
rgbStrip.setState("off") # off
sendConsole = 0
pcf8523Available = False # 2.2.28   2.2.27
bme280Available = False
hm3300Available = False
# 2.2.18 ads1115Available = False
sgp30Available = False
time_sgp30 = 0 # 2.2.1b


def jsonToStr(data):
    return str(data).replace("'", '"').replace('True', 'true').replace('False','false')


if Skyhook == False:  # 2.2.2b
#        internalSettings[key] = value
#    else: #
#        internalSettings[key] = configurator.readVRAM(key) # FIX/BUG: battery_charge does not always load correctly soetimes loads as None
#        print('Loading internalSetting: {} = {}'.format(key, internalSettings[key]), debugging=True)

    bme280 = BME280(0x76, i2c=I2C1, sdAccess=sdAvailable) # J5 or J7
    hm3300 = HM3300(0x40, i2c=I2C1, socketVersion=deviceSettings['pycom_socket_version'], sdAccess=sdAvailable) # J6 (prefered) or J1
    sgp30 = SGP30(IamAlive, 0x58, i2c=I2C1, sdAccess=sdAvailable) # 2.2.20  J5 or J7
    ads1115 = ADS1115(0x48, i2c=I2C1, reference='Grove_Batt-PID', sdAccess=sdAvailable) # cf     PID, power < 85 mW at 3.2 V (26mA), < 300 mW transient for 200ms (93mA)

    # 2.2.0b
    wsoIC1 = ADS1115(0x4B, i2c=I2C1, reference='WS_IC1', sdAccess=sdAvailable)
    wsoIC4 = ADS1115(0x4A, i2c=I2C1, reference='WS_IC4', sdAccess=sdAvailable)
    wsoIC3 = ADS1115(0x49, i2c=I2C1, reference='WS_IC3', sdAccess=sdAvailable)
    wsoIC6 = PCF8574(0x038, i2c=I2C1, reference='WS_IC6', sdAccess=sdAvailable) # TI    Philipps 0x20)

    # 2.2.27     ds1307 = DS1307(i2c=I2C1, sdAccess=sdAvailable)
    pcf8523 = PCF8523(i2c=I2C1, sdAccess=sdAvailable)  # 2.2.28
    if SocketVersion == 4:
        try:
            puIC6 = PCF8574(0x039, i2c=I2C1, reference='PB_IC100', sdAccess=sdAvailable) # cf3/15
            swNetAvailable = puIC6.begin()
            puIC6.clrLive()
        except:
            puIC6 = PCF8574(0x021, i2c=I2C1, reference='PB_IC100', sdAccess=sdAvailable)
            swNetAvailable = puIC6.begin()
            puIC6.clrLive()
        # PB_IC100 same functionality as PU_IC6 but different board
    else:
        puIC6 = PCF8574(0x039, i2c=I2C1, reference='PU_IC6', sdAccess=sdAvailable)
        swNetAvailable = puIC6.begin()
    adc6814 = MCP3424(iicadr=0x069, i2c=I2C1, sdAccess=sdAvailable) # 2.3.2
    mps = MPSBridge(iicadr=0x048, i2c=I2C1, traceDebug=False, sdAccess=sdAvailable) # 2.4.14

# 2.2.18 wsoAvailable = False  #  I2C switch present
wsOptionAvailable = False  # 2.2.20    WS optional hardware detected
time_wind = 0

swNetAvailable = False # 2.3.1
adc6814Available = False # 2.3.2
mpsAvailable = False # 2.4.14

# 2.2.1b
# 2.2.18 fuelgaugeAvailable = False
time_fuel = 0



# 2.2.2b   Skyhook
if Skyhook:
    # RGB LED
    rgbStrip1 = WS2812(ledNumber=60, dataPin='P2')
    rgbStrip2 = WS2812(ledNumber=60, dataPin='P3')
    rgbStrip3 = WS2812(ledNumber=60, dataPin='P9')
    rgbStrip4 = WS2812(ledNumber=60, dataPin='P10')

    # Weather sensors
    # Temp Barometer sensor
    BarSens = BMP280(0x77, i2c=I2C1, sdAccess=sdAvailable)
    # PM Dust sensor
    DustSens = HM3300(0x40, i2c=I2C1, socketVersion=deviceSettings['pycom_socket_version'], sdAccess=sdAvailable)

    Acc = LIS3DH(0x019, i2c=I2C1, sdAccess=sdAvailable)   # accelerometer

    # Relay board
    # Current power monitor INA219
    IVCh4 = INA219(0x48, 0.002, i2c=I2C1, traceDebug=True, sdAccess=sdAvailable)
    IVCh2 = INA219(0x49, 0.002, i2c=I2C1, traceDebug=False, sdAccess=sdAvailable)
    IVCh3 = INA219(0x4A, 0.002, i2c=I2C1, traceDebug=False, sdAccess=sdAvailable)
    IVCh1 = INA219(0x4B, 0.002, i2c=I2C1, traceDebug=False, sdAccess=sdAvailable)
    IVCh7 = INA219(0x4C, i2c=I2C1, sdAccess=sdAvailable)
    IVCh5 = INA219(0x4D, i2c=I2C1, sdAccess=sdAvailable)
    IVCh8 = INA219(0x4E, i2c=I2C1, sdAccess=sdAvailable)
    IVCh6 = INA219(0x4F, i2c=I2C1, sdAccess=sdAvailable)

    TempBatt = MCP9808(0x018, i2c=I2C1, sdAccess=sdAvailable)
    TempFitlet = MCP9808(0x01A, i2c=I2C1, sdAccess=sdAvailable)

    DIO = MCP23017(0x21, 0x0000, i2c=I2C1, sdAccess=sdAvailable)  # All line are output


# 2.2.2b   Skyhook
thrAccX, thrAccY, thrAccZ = 0.1, 0.1, 0.1  # Threshold to trigger alarm tilt
zeroAccX, zeroAccY, zeroAccZ = 0, 0, 0  # Base at power up

BarSensOnline = False
DustSensOnline = False
AccSensOnline = False
IVSensOnline = False # If any of sensor of the relay board is measing trigger False
TempBattOnLine = False  #  TempSensOnLine = False
TempFitletOnLine = False
wss = {}


IamAlive() # 2.2.20  wdt.feed()


# 8b  GPS
GPSLat = 0   # 30.4548
GPSLong = 0  # -97.5905
GPSAccuracy = 1  # a value of 0.0 is perfect
l76Available = False
l76 = None # updated in coldInit()
py = None # 8b

# https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
def two_pass_variance(data):
    n = sum1 = sum2 = 0

    for x in data:
        n += 1
        sum1 += x

    mean = sum1 / n

    for x in data:
        sum2 += (x - mean) * (x - mean)

    variance = sum2 / (n - 1)
    return variance


# for best data use an active antenna part# 604565430465 Mouser
# Jump connctor P3 of Pytrack board
def GPSFetch(debug=False): #8b
    global l76Available, GPSLat, GPSLong, GPSAccuracy

    ZLat = 0
    ZLong = 0
    ZLoop = 0
    LatPacket = []
    LongPacket = []
    LatVariance = 0
    LongVariance = 0
    GPSAccuracy = 1
    # Init takes place in coldInit()   l76Available = True # l76.begin()
    if l76Available == False:
        return

    try:
        # for best resolution, 7 request to pick up 7 sat if available
        for x in range(7): # when all packets are equal the coordinates is OK !!
            IamAlive() # 2.2.20
            time.sleep(1)
            GPSLat, GPSLong = l76.coordinates(debug=debug)  # l76.dump_nmea()  dump all the data from GPS
            print("GPS Lat:" + str(GPSLat) + " Long:" + str(GPSLong))
            if not(GPSLat == None):
                LatPacket.append(GPSLat)
                LongPacket.append(GPSLong)
                ZLat = ZLat + GPSLat
                ZLong = ZLong + GPSLong
                ZLoop = ZLoop + 1
            # 2.2.6b
            if GPSLat == None or GPSLong == None:
                ZLoop = 0
                break

        print("GPS Lat:" + str(LatPacket))
        print("GPS Long:" + str(LongPacket))
        if not(ZLoop == 0):
            if ZLoop == 7: # in any case all packets should be valid
                LatVariance = two_pass_variance(LatPacket)
                LongVariance = two_pass_variance(LongPacket)
                GPSAccuracy = (LatVariance + LongVariance) / 2 # evaluate reproducibility of data
                print("GPS LatVar:" + str(LatVariance) + " LongVar:" + str(LongVariance))
            else:
                GPSAccuracy = 1
            GPSLat = ZLat / ZLoop
            GPSLong = ZLong / ZLoop

        else:
            GPSLat = 0
            GPSLong = 0
            GPSAccuracy = 1

        print("GPS Loop:" + str(ZLoop) + " Accuracy:" + str(GPSAccuracy))
        print("GPS Lat:" + str(GPSLat) + " Long:" + str(GPSLong))

    except Exception as e: # 2.2.9b
        file_errLog(0, "Err FetchGPS - " + str(e), sdAvailable)  # 2.2.9b    print("Err FetchGPS")
        pass


def GPSpowerMode(pw): # 8b  0= Stdby,  1= Normal
    global l76Available # 9b

    if deviceSettings['pycom_socket_version']  == 3:
        if pw == 1: # power ON
            py.gps_standby(enabled=False)
            # 9b  py.sensor_power(enabled=True)
            time.sleep(1)
        else:
            print('put GPS in standby')
            py.gps_standby(enabled=True)  # py.gps_standby_2(enabled=True)  9b
            # 9b py.sensor_power(enabled=False)  # Turn off power to GPS and sensors (Pyport pin#1)
            # py.gps_standby_2(enabled=
            # py.gps_reset(enabled=
            l76Available = False # 9b

'''
# 2.2.10b       retry GPS get automaticaly
if l76Available == True:
    GPSFetch(debug=False) # True)
    if abs(GPSAccuracy) < 0.001:  # data valid, turn off power to GPS
        # send MQTT ....
        GPSpowerMode(0)
        pass
    else:
        print("GPS failled to acquire good data !!!")
'''


'''
if deviceSettings['pycom_socket_version']  == 3:
    GPSFetch(debug=False) # 8b
    # for debug    l76.dump_nmea()
'''



# 2.2.2b
def file_rgbConfig(rw, animate = False):
    try :
        if rw == 0:  # write file
            if animate == False:
                rgbStripList = [(0,0,0) ] *4   # list of tupple [()]
                filehandle = open('/sd/rgbConfig.txt', 'w')
                rgbStripList[0] = rgbStrip1.np[0]
                rgbStripList[1] = rgbStrip2.np[0]
                rgbStripList[2] = rgbStrip3.np[0]
                rgbStripList[3] = rgbStrip4.np[0]
            else:
                rgbStripList = [(0,0,0,0,0) ] *4
                filehandle = open('/sd/rgbAnimate.txt', 'w')
                r,g,b = rgbStrip1.color
                rgbStripList[0] = rgbStrip1.effect, r, g, b, rgbStrip1.delay
                r,g,b = rgbStrip2.color
                rgbStripList[1] = rgbStrip2.effect, r, g, b, rgbStrip2.delay
                r,g,b = rgbStrip3.color
                rgbStripList[2] = rgbStrip3.effect, r, g, b, rgbStrip3.delay
                r,g,b = rgbStrip4.color
                rgbStripList[3] = rgbStrip4.effect, r, g, b, rgbStrip4.delay

            json.dump(rgbStripList, filehandle)  # change [()] to [[]]
            filehandle.close()
            print ('Write rgbConfig')
        else:
            if animate == False:
                rgbStripList = [[0,0,0] ] *4  # Prepare to read [[]]  list of list
                filehandle = open('/sd/rgbConfig.txt', 'r')
                rgbStripList = json.load(filehandle)
                red,green,blue = rgbStripList[0]
                rgbStrip1.setColor(red,green,blue)
                red,green,blue = rgbStripList[1]
                rgbStrip2.setColor(red,green,blue)
                red,green,blue = rgbStripList[2]
                rgbStrip3.setColor(red,green,blue)
                red,green,blue = rgbStripList[3]
                rgbStrip4.setColor(red,green,blue)
            else:
                rgbStripList = [(0,0,0,0,0) ] *4
                filehandle = open('/sd/rgbAnimate.txt', 'r')
                rgbStripList = json.load(filehandle)
                effect,r,g,b,delay = rgbStripList[0]
                rgbStrip1.animate(effect,r,g,b,delay)
                effect,r,g,b,delay = rgbStripList[1]
                rgbStrip2.animate(effect,r,g,b,delay)
                effect,r,g,b,delay = rgbStripList[2]
                rgbStrip3.animate(effect,r,g,b,delay)
                effect,r,g,b,delay = rgbStripList[3]
                rgbStrip4.animate(effect,r,g,b,delay)

            filehandle.close()
            print('Read rgbConfig')

    except Exception as e: # 2.2.9b
        file_errLog(0, "Failed access rgbConfig file - " + str(e), sdAvailable)  # 2.2.9b    print('Failed access rgbConfig file')



# 2.09 Pulse one of the coil of the double coil relay  (23mA)
def set_Relay(StateRelay, ONCode, OFFCode):
    if Skyhook == False: # 2.2.2b
        # 6b  if deviceSettings['pycom_socket_version'] == 1:
        if not(deviceSettings['pycom_socket_version'] == 2) or not(eviceSettings['pycom_socket_version'] == 20): # 2.2.5b
            return

        if DIOAvailable: # 2.2.18
            if StateRelay:
                DIO.writePort(ONCode)   # Set
            else:
                DIO.writePort(OFFCode)   # Reset
            time.sleep(.2)
            DIO.writePort(0x0FF)

    else: # 2.2.2b
        if StateRelay:
            DIO.writePort(OFFCode)   # Set
        else:
            DIO.writePort(ONCode)   # Reset
        time.sleep(.2)  # sleep_ms (100)
        DIO.writePort(0)


''' 2.2.18  duplicate name ?!
def set_Relay(StateRelay, ONCode, OFFCode):
    if StateRelay:
        DIO.writePort(OFFCode)   # Set
    else:
        DIO.writePort(ONCode)   # Reset
    time.sleep(.2)  # sleep_ms (100)
    DIO.writePort(0)
'''



# 2.09
def do_Relay(idRelay, StateRelay):
    # Quick command to open all relays no matter StateRelay
    if (idRelay == 0): set_Relay(False, 0x05, 0x05) #  0101 activate reset coil
    #
    if (idRelay == 1): set_Relay(StateRelay, 0x0E, 0x0D) # 1110, 1101)  # P0, p1 / ON, OFF
    if (idRelay == 2): set_Relay(StateRelay, 0x0B, 0x07) # 1011, 0111)  # p2, p3 / ON, OFF


# 2.2.2b
def skyhook_Relay(idRelay, StateRelay):
    # Quick command to open all relays no matter StateRelay
    # 1 + 0x010 + 0x08000 + 0x0800 + 0x040 + 0x02000 + 4 + 0x0200
    if (idRelay == 0): set_Relay(False, 0x055AA, 0x055AA) #  Quick command to open all relays
    #
    if (idRelay == 1): set_Relay(StateRelay, 2, 1)  # A1, A0 / ON, OFF   b10 b1
    if (idRelay == 2): set_Relay(StateRelay, 0x020, 0x010)  # A5, A4     b10_0000  b1_0000
    if (idRelay == 3): set_Relay(StateRelay, 0x04000, 0x08000)  # B6, B7
    if (idRelay == 4): set_Relay(StateRelay, 0x0400, 0x0800)  # B2, B3
    if (idRelay == 5): set_Relay(StateRelay, 0x080, 0x040)   # A7, A6  b1000_0000  b100_0000
    if (idRelay == 6): set_Relay(StateRelay, 0x01000, 0x02000)  # B4, B5
    if (idRelay == 7): set_Relay(StateRelay, 8, 4)  # A3, A2    b1000  b100
    if (idRelay == 8): set_Relay(StateRelay, 0x0100, 0x0200)  # B0, B1


# 2.2.2b  from K1 to K8 list the hours range the relay is ON
kSCheduleList = [[1,2],[3,4],[10,11],[7,8],[9,10],[11,12],[13,14],[15,16]]



# 2.2.2b
def skyhook_Relays():
    global kSCheduleList

    if (IVSensOnline == 0): return
    for i in range(8):
        StateRelay = False
        if (kSCheduleList[i][0] == 1):
            StateRelay = True

        skyhook_Relay(i+1, StateRelay)
        print("Relay Ch %d is "%(i+1) + str(StateRelay) + "/" + str(kSCheduleList[i][0]) )


# 2.2.2b
def file_KConfig(rw):
    # see boot.py for example of file access
    # https://stackabuse.com/reading-and-writing-lists-to-a-file-in-python/
    global kSCheduleList

    try :
        if rw == 0:  # write file
            filehandle = open('/sd/KConfig.txt', 'w')
            json.dump(kSCheduleList, filehandle)
            filehandle.close()
            print('Write KConfig')
        else:
            filehandle = open('/sd/KConfig.txt', 'r')
            kSCheduleList = json.load(filehandle)
            if len(kSCheduleList) == 8:
                print('Read KConfig')
            else:
                kSCheduleList = [[1,1]] * 8
                print('Error data KConfig')
            filehandle.close()

    except Exception as e: # 2.2.9b
        kSCheduleList = [[1,1]] * 8
        file_errLog(0, "Failed access KConfig file - " + str(e), sdAvailable)  # 2.2.9b   print('Failed reading KConfig file')



# 2.2.17
# discover sample catcher pumps ...
# this function must be called as soon as the connection to the sample catcher is made
# if it not excuted PI4M and BME280 will not work well because of adr conflict on I2C
smp7Available = smp6Available = smp5Available = smp4Available = False
smp3Available = smp2Available = smp1Available = smp0Available = False
def catcherInit():
    global catcherAvailable
    global smp7Available, smp6Available, smp5Available, smp4Available
    global smp3Available, smp2Available, smp1Available, smp0Available

    if deviceSettings['catcher_enabled']: # 2.2.18
        catcherAvailable = LEDIO.begin()
        if catcherAvailable:
            smp7Available = EEP7.begin()  # EEP7.fileHeader
            smp6Available = EEP6.begin()
            smp5Available = EEP5.begin()
            smp4Available = EEP4.begin()
            smp3Available = EEP3.begin()
            smp2Available = EEP2.begin()
            smp1Available = EEP1.begin()
            smp0Available = EEP0.begin()

# 2.2.17
def catcherCmd(Pump, Cmd):
    global catcherAvailable
    global smp7Available, smp6Available, smp5Available, smp4Available
    global smp3Available, smp2Available, smp1Available, smp0Available

    if catcherAvailable:
        if smp7Available and Pump == 7: LEDIO.ledCmd(15, Cmd)   # for debug VA   (7, Cmd)
        if smp6Available and Pump == 6: LEDIO.ledCmd(13, Cmd)
        if smp5Available and Pump == 5: LEDIO.ledCmd(11, Cmd)
        if smp4Available and Pump == 4: LEDIO.ledCmd(9, Cmd)
        if smp3Available and Pump == 3: LEDIO.ledCmd(7, Cmd)
        if smp2Available and Pump == 2: LEDIO.ledCmd(5, Cmd)
        if smp1Available and Pump == 1: LEDIO.ledCmd(3, Cmd)
        if smp0Available and Pump == 0: LEDIO.ledCmd(1, Cmd)


def swNet_led(led=False): # 2.3.1
    if swNetAvailable == False:  # 2.3.12
        return
    if SocketVersion == 4: # 2.3.12
        puIC6.swNet_led(led)
    else:
        if led:
            puIC6.writePort( 0b11111101)
        else:
            puIC6.writePort( 0xFF)


def swNet_read():  # 2.3.1
    if swNetAvailable:
        u1 = puIC6.readPort() & 0x01
        if (u1 == 0):
            #swNet_led(True)
            return True
        else:
            #swNet_led(False)
            return False  # 0 = button pushed
    else:
        return False


i2cScan = [] # 2.2.18
battV = 4 # 2.3.8    3.45
battI = 0
time_dust = deviceSettings['pm_interval']
tvocBaseline, co2eBaseline = 0, 0 # 5b

# 2.3.6
# FRAM variables moved to json file, the json file is stored on both sd and Flash
# when reading the file the data come from the flash file first, if error use backup on sd card, if failing still, look for data in FRAM
# at compilation, the json file is installed on flash, it can be edited with Atom
# if you need to add or edit a variable, edit FRAM.json
framPacket = {"battery_charge": 0, "pid_offset": 0, "pid_gain": 0, "tvoc_base": 0, "co2_base": 0, \
"c1_offset": 0, "c1_gain": 0, "c1_range": 0, "co2_offset": 0, "co2_gain": 0, "co2_range": 0,  \
"h2s_offset": 0, "h2s_gain": 0, "h2s_root": 0, "o3_offset": 0, "o3_gain": 0, "o3_root": 0, \
"resp_offset": 0, "resp_gain": 0, "resp_root": 0, "so2_offset": 0, "so2_gain": 0, "so2_root": 0, \
"red_r0": 0, "ox_r0": 0, "nh3_r0": 0, "co_offset": 0, "co_gain": 0, "co_root": 2, "no2_offset": 0, "no2_gain": 0, "no2_root": 1}


# 2.2.0b nvsKeys = ['battery_charge', 'pid_offset', 'pid_gain', 'tvoc_base', 'co2_base']
nvsKeys = ['battery_charge', 'pid_offset', 'pid_gain', 'tvoc_base', 'co2_base', \
'c1_offset', 'c1_gain', 'c1_range', 'co2_offset', 'co2_gain', 'co2_range',  \
'h2s_offset', 'h2s_gain', 'h2s_root', 'o3_offset', 'o3_gain', 'o3_root', \
'resp_offset', 'resp_gain', 'resp_root', 'so2_offset', 'so2_gain', 'so2_root', \
'red_r0', 'ox_r0', 'nh3_r0', 'co_offset', 'co_gain', 'co_root', 'no2_offset', 'no2_gain', 'no2_root']  # 2.3.4
# 2.3.6  internalSettings = configurator.readVRAMBulk(nvsKeys)
# cf3/22  internalSettings = {'tvoc_base': 0, 'co2_base': 0}
# 2.3.6
# copy FRAM to framPacket
# call that function once after updating the program
# then use the function writeFram() to create a json file of the Fram parameters
# usage: # copyFramPacket(nvsKeys)
def copyFramPacket(keys):
    for key in keys:
        try:
            framPacket[key] = str(pycom.nvs_get(key) / 1000)
        except:
            print('Err key:' + key)


# 2.3.10   ''' 2.3.6  if you need to add or edit a variable, edit FRAM.json
def defaultInternalSetting(key,value):
    global internalSettings

    if key not in internalSettings or internalSettings[key] == None:
        print('Creating default internal setting: {} = {}'.format(key, value), debugging=True)
        configurator.writeVRAM(key, value)
        internalSettings[key] = value  # 5b

# 2.3.10
def defaultInternalSettings():
    file_errLog(0, "Restore default internalSettings", sdAvailable)
    defaultInternalSetting('battery_charge', 47520) # might be too long of a key
    defaultInternalSetting('pid_gain', 1000 / 0.025) # ppb per mV
    defaultInternalSetting('pid_offset', 0.046)
    defaultInternalSetting('co2_base', 0)
    defaultInternalSetting('tvoc_base', 0)

    # 2.2.0b
    # NDIR CO2   NDIR CH4  range 5%
    # Output voltage for zero 0.1V
    # Output voltage for span 2.0V   (5000ppm)
    defaultInternalSetting('co2_offset', 0.1) # 0.1V @ 0%
    defaultInternalSetting('co2_gain', 26315.789)  # 2.2.11b  VSpan(2.0 - 0.1)  @ 5%     1.9V = 5%      V * 50000 = 1.9 * C       C = V * 50000 / 1.9
    defaultInternalSetting('co2_range', 300000) # 2.2.24b
    defaultInternalSetting('c1_offset', 0.1)
    defaultInternalSetting('c1_gain', 26315.789) # 2.2.11b
    defaultInternalSetting('c1_range', 50000) # 2.2.24b

    # 2.2.0b
    # 2.3.4 ADC root declaration to SPEC sensor    U4=IC1_ch0=0   U3=IC1_ch2=1   U7=IC4_ch0=2    U6=IC4_ch2=3
    defaultInternalSetting('h2s_offset', 1.407) # offset = SP - SN   @ 0 gas
    defaultInternalSetting('h2s_gain', 133.467)  # 1/M =  1 / ( [162.96] * 49.9 *10-6) = 122.9754
    defaultInternalSetting('h2s_root', 2) # 2.3.4   U7 default

    defaultInternalSetting('o3_offset', 1.391) # 2.2.24
    defaultInternalSetting('o3_gain', -46.143) # 2.2.24
    defaultInternalSetting('o3_root', 0) # 2.3.4

    defaultInternalSetting('co_offset', 1.391) # 2.2.24
    defaultInternalSetting('co_gain', 2.6) # 2.2.24
    defaultInternalSetting('co_root', 0) # 2.3.4

    defaultInternalSetting('no2_offset', 1.391) # 2.2.24
    defaultInternalSetting('no2_gain', -27.02) # 2.2.24
    defaultInternalSetting('no2_root', 0) # 2.3.4

    defaultInternalSetting('resp_offset', 1.317) # 2.3.1
    defaultInternalSetting('resp_gain', -44.2093) # 2.3.1
    defaultInternalSetting('resp_root', 3) # 2.3.4  U6 default

    defaultInternalSetting('so2_offset', 1.4) # 2.3.3
    defaultInternalSetting('so2_gain',349.895) # 2.3.3
    defaultInternalSetting('so2_root', 3) # 2.3.4   U6 default

    defaultInternalSetting('red_r0', 100000) # 2.3.2   100K to 1500K
    defaultInternalSetting('ox_r0', 800) # 2.3.2    0.8K  to 20K
    defaultInternalSetting('nh3_r0', 10000) # 2.3.2   10K to 1500K


import os # 2.3.10
import sys # 2.3.10

# 2.3.10  relocate     2.3.6
# before shutting down, execute this function to save the Fram variables to disks
def writeFram():
    global framPacket, internalSettings

    framPacket = internalSettings
    try:
        f = open('/flash/fram.json', 'w+') # 2.3.10   'a')
        # f.truncate(0) # 2.3.10 f.seek(0)
        f.write(json.dumps(framPacket))  # convert framPacket into json
        f.close()
        print("Write framPacket to /flash/")
    except:
        print("writeFram() error writing fram.json to flash")

    try:
        f = open('/sd/fram.json', 'w+') # 2.3.10   a')
        # f.truncate(0) # 2.3.10   f.seek(0)
        f.write(json.dumps(framPacket))
        f.close()
        print("Write framPacket to /sd/")
    except:
        print("writeFram() error writing fram.json to sd")

    print("")


# 2.3.6
def readFram(): # if fails reading Fram.json, use data from Fram
    global framPacket, internalSettings
    try:
        f = open('/flash/fram.json', 'r')
        # 2.3.10  f.seek(0)
        buffer = f.read()
        f.close()
        framPacket = json.loads(buffer) # internalSettings = framPacket
        print("Read framPacket from /flash/")
        internalSettings = framPacket
    except:
        try:
            if sdAvailable: # 2.3.10
                f = open('/sd/fram.json', 'r')
                # 2.3.10 f.seek(0)
                buffer = f.read()
                f.close()
                framPacket = json.loads(buffer)
                print("Read framPacket from /sd/")
                internalSettings = framPacket
            else:
                print("Restore default internalSettings")
                defaultInternalSettings() # 2.3.10
                # framPacket = configurator.readVRAMBulk(nvsKeys)
        except:
            print("readFram() error accesing fram.json")
            print("Restore default internalSettings")
            defaultInternalSettings() # 2.3.10
            # framPacket = configurator.readVRAMBulk(nvsKeys)
    print("")


readFram() # 2.3.6
print("internalSettings:") # 2.3.10
print(internalSettings)



sgp30TempThreshold = 0 # cf3/7
sgp30HumidityThreshold = 0  # cf3/7


# Control dust sensor ON/OFF over all platforms
def powerPM(pw = 0): # cf3/15
    if deviceSettings['pycom_socket_version']  == 3 or deviceSettings['pycom_socket_version']  == 21:
        if DIOAvailable:
            if pw == 1:
                DIO.wakeupPM()
            else:
                DIO.sleepPM()
    if deviceSettings['pycom_socket_version']  == 4:
        if swNetAvailable:
            if pw == 1:
                puIC6.wakeupPM()
            else:
                puIC6.sleepPM()
    if hm3300Available:
        hm3300.powerMode(pw)


if Skyhook == False: # 2.2.2b
    def coldInit(): # REEVAL - with global battery settings and gauge
        global time_dust, battV # CF
        global tvocBaseline, co2eBaseline # 5b
        global hm3300Available, bme280Available, sgp30Available, ads1115Available
        global mpsAvailable, deviceSettings
        global py, l76, l76Available # 8b
        global time_wind, wsoAvailable, wsOptionAvailable # 2.2.20   2.2.0b
        global fuelgaugeAvailable # 2.2.1b
        global i2cScan # 2.2.18
        global pcf8523Available, swNetAvailable, adc6814Available # 2.3.2
        global sgp30TempThreshold, sgp30HumidityThreshold # cf3/7

        print( "coldInit")
        print(I2C1.scan()) # DBUG
        if deviceSettings['pycom_socket_version']  == 2 or deviceSettings['pycom_socket_version']  == 20: # 2.2.5b
            if DIOAvailable: # 2.2.18  if DIO.begin():
                do_Relay(0,False)  # Open all the relays (off)
                do_Relay(1,True)  # Close K1 (Power on external Grove)
                time.sleep(3)

        # 2.2.0b  Power to weather shield turned ON
        if deviceSettings['pycom_socket_version']  == 3 or deviceSettings['pycom_socket_version']  == 21: # 2.2.12b
            if DIOAvailable: # 2.2.18  if DIO.begin():
                print( "Turn ON power to weathershield.")
                # 2.2.21  DIO.writePort(0x03) # Set P0(Power shield)=1 &  P1(PM)=1
                DIO.shieldON() # 2.2.21
                DIO.wakeupPM() # 2.2.21
                time.sleep(3)

        # 2.2.28
        pcf8523Available = pcf8523.begin()
        if pcf8523Available:
            print('RTC PCF8523 time is:' + str(pcf8523.datetime()))
            rtc.init(pcf8523.datetime())
        # 2.3.1
        swNetAvailable = puIC6.begin()
        if swNetAvailable: print('Network Sw Poke is detected')

        powerPM(1) # cf3/15

        # 2.3.8    2.3.2
        # adc6814Available = adc6814.begin()
        # if adc6814Available:  print('RED, OX, NH3 sensors detected')

        # 2.2.0b
        # weather_shield_option = 159 $9F
        #              b0 = IR_CO2 U2  IC3 $49  AIN_0        # Vb  IR_CO2 U8  IC1 $4B  AIN_0P-1N
        #              b1 = ir_c1 U5  IC1 $4B  AIN_2P-3N
        #              b2 = PID U1     IC3 $49  AIN_2P-3N
        #              b3 = H2S U7     IC4 $4A  AIN_0P-1N
        #              b4 = Wind speed IC6 $20
        #                   Wind dir   IC3 $49 AIN_1
        #              b7 = weather shield PCB
        # deviceSettings['weather_shield_option'] = 1 | 2 | 4 | 8 | 16 | 128 # CO2 CH4 PID H2S Wind   for debug only
        if deviceSettings['weather_shield_option'] > 0:
            print( "Init optional sensors.")
            # 2.2.18 wsoAvailable = wsCommSw.begin()
            if wsoAvailable == True:
                wsCommSw.channel(0x05)  # weather shield - enable I2C channel 1   Adr112
                time.sleep(0.1) # 2.2.18

            # 2.4.14
            mpsAvailable = mps.begin()

            # 2.3.8
            if (deviceSettings['weather_shield_option'] & 0b01000000000) != 0: #  b9 = 6814  RED, OX, NH3
                adc6814Available = adc6814.begin()
                if adc6814Available:  print('RED, OX, NH3 sensors detected')

            # 2.2.23b for compatibility with WS VC3
            #
            # 2.2.18  look for chips prior comm ...   pycom scocket 54,65
            # 56, 64, 73, 74, 75, 88, 112, 118   these are weathershield
            # i2cScan.index(74)    crash if not in list
            # 74 in i2cScan      return True / False
            i2cScan = I2C1.scan()
            print(i2cScan)
            wsOptionAvailable = False # 2.2.20   wsoAvailable = False
            bool1 = (56 in i2cScan) and (73 in i2cScan) and (74 in i2cScan) and (75 in i2cScan)
            if bool1: # 2.2.18
                # if (deviceSettings['weather_shield_option'] & 16) != 0: # wind sensors ?
                time_wind = 60
                wsOptionAvailable = wsoIC6.begin() # 2.2.20   wsoAvailable &= wsoIC6.begin() # speed   Adr56
                wsOptionAvailable &= wsoIC3.begin() # 2.2.20   wsoAvailable &= wsoIC3.begin() # direction
                # elif (deviceSettings['weather_shield_option'] & 4) != 0: # PID ?
                #    wsoAvailable &= wsoIC3.begin()

                # if (deviceSettings['weather_shield_option'] & 3) != 0: # IR sensors ?
                wsOptionAvailable &= wsoIC1.begin() # 2.2.20  wsoAvailable &= wsoIC1.begin()

                # if (deviceSettings['weather_shield_option'] & 8) != 0: # H2S sensor ?
                wsOptionAvailable &= wsoIC4.begin() # 2.2.20  wsoAvailable &= wsoIC4.begin()

            else: # 2.2.18
                file_errLog(0, "Err I2C on WeatherShield", sdAvailable)

            # 2.2.23b  for compatibility with WS VC3
            #
            # else: # 2.2.18
            #    file_errLog(0, "WeatherShield IIC switch not found", sdAvailable)
            #    i2cScan = I2C1.scan()
            #    print(i2cScan)

        # 2.2.0b  wait for weathershield I2C to be initialized
        time_dust = deviceSettings['pm_interval']  # CF  Dust sensor measur period
        if (118 in i2cScan): # 2.2.18  do not mount if chip not present
            bme280Available = bme280.begin()  # Adr118
        else: # 2.2.23b
            file_errLog(0, "BME280 (Temp/Humidity/Pressure) not found", sdAvailable)
        if (64 in i2cScan): # 2.2.18
            hm3300Available = hm3300.begin() # starts with power on  Adr64
        else: # 2.2.23b
            file_errLog(0, "Dust sensor not found", sdAvailable)
        if (88 in i2cScan): # 2.2.18
            sgp30Available = sgp30.begin()  # Adr88
            if (not sgp30Available):
                print("Trying again...")
                time.sleep_ms(2000);
                sgp30Available = sgp30.begin()
            if bme280Available and sgp30Available: # 2.3.12  calibrate tvoc with humidity
                temperature, pressure, humidity = bme280.read()
                sgp30TempThreshold = temperature
                sgp30HumidityThreshold = humidity
                if temperature != 0 and pressure != 0 and humidity != 0: # 2.2.21
                    dv = sgp30.calculate_dv(temperature, humidity)
                    sgp30.set_humidity(dv)

                # cf3/7
                co2eBaseline, tvocBaseline = sgp30.get_baseline("ColdInit")

                if 'tvoc_base' in internalSettings:
                    tvocBaseline = int(internalSettings['tvoc_base'])
                if 'co2_base' in internalSettings:
                    co2eBaseline = int(internalSettings['co2_base'])
                if ((co2eBaseline != 0 and co2eBaseline != None) and (tvocBaseline != 0 and co2eBaseline != None)):
                    sgp30.set_baseline(tvocBaseline, co2eBaseline, "ColdInit")
                else:
                    print("Coldinit - disregard VOC baseline restore")

        else: # 2.2.23b
            file_errLog(0, "VOC sensor not found", sdAvailable)


        # 8b
        if deviceSettings['pycom_socket_version']  == 3:
            from pytrack import *
            py = Pytrack()
            GPSpowerMode(1)
            IamAlive() # 2.2.20
            time.sleep(1)
            l76 = L76GNSS(py, timeout=30, sdAccess=sdAvailable) # 2.2.20    # , buffer=512)
            l76Available = l76.begin()
            if l76Available == True:
                print( "Waiting 60sec for GPS to boot .", endln='')
                for x in range(60):  # allow 60sec for GPS to acquire sat data
                    IamAlive() # 2.2.20
                    time.sleep(1)
                    print('.', endln='')
                print('!')
                GPSFetch(debug=False) # True)
                if abs(GPSAccuracy) < 0.001:  # data valid, turn off power to GPS
                    GPSpowerMode(0)
                    pass
                else:
                    print("GPS failled to acquire good data !!!")


        if hm3300Available: # 5b  power up dust sensor
            hm3300.powerMode(1)

        ''' # 2.3.11 disabled
        # 2.2.16b - restored in 2.3.8
        if sgp30Available: # 5b restore VOC baselines
            # 2.3.10 print(internalSettings)
            tvocBaseline = 0 # 2.3.11
            co2eBaseline = 0 # 2.3.11
            if 'tvoc_base' in internalSettings:
                tvocBaseline = int(internalSettings['tvoc_base'])
            if 'co2_base' in internalSettings:
                co2eBaseline = int(internalSettings['co2_base'])
            if ((co2eBaseline != 0 and co2eBaseline != None) and (tvocBaseline != 0 and co2eBaseline != None)):
                print(tvocBaseline, co2eBaseline)
                sgp30.set_baseline(tvocBaseline, co2eBaseline)
        '''

        # 2.2.1b  battery charge on boot
        battV = deviceSettings['low_battery_voltage_threshold'] # if battery reading unavailable assume good?
        # 2.2.18 fuelgaugeAvailable = fuelGauge.begin()
        if fuelgaugeAvailable == True:  # if available use fuelGauge IC  instead of ADC to count coulomb
            battV = fuelGauge.read_vcell()
            battGauge = fuelGauge.read_SOC()
            battCapacity = deviceSettings['battery_size'] * 3600 # 2.3.5
            # 2.3.5  battCsum = (battGauge * 47520) / 100  # 100 = 47520     SOC = n    n =  SOC * 47520 / 100
            battCsum = (battGauge * battCapacity) / 100  # 2.3.5
            internalSettings['battery_charge'] = battCsum
            print("Battery gauge (%): " + str(battGauge) )
        else:
            # 2.2.18 ads1115Available = ads1115.begin()
            pass

        # estimate battery charge on boot
        if ads1115Available == True:  # 2.07
            # 2.2.5b
            # if deviceSettings['pycom_socket_version']  == 2 or deviceSettings['pycom_socket_version']  == 3: # 8b
            #    battV = ads1115.get_voltage(ads1115.read(channel=1, gain=PGA_2_048V)) # 2.09
            # else: # 2.09
            #    battV = ads1115.get_voltage(ads1115.read(channel=2, gain=PGA_2_048V))
            #
            if deviceSettings['pycom_socket_version']  == 1: # 2.2.5b
                battV = ads1115.get_voltage(ads1115.read(channel=2, gain=PGA_2_048V))
            else:
                battV = ads1115.get_voltage(ads1115.read(channel=1, gain=PGA_2_048V))
            battV = battV * 220 / 100  #  https://ohmslawcalculator.com/voltage-divider-calculator
            battCapacity = deviceSettings['battery_size'] * 3600 # 2.3.5
            if (battV >= 4.15):
                battCsum = battCapacity * 0.95 # 2.3.5    47520 * 0.95 # Charge set 95%
            else:
                # 4.14 - 3.29 =>  0.85 = 94%
                # (battV - 3.3) = n%
                battGauge = (battV - 3.3) * 94 / 0.85
                battCsum = ( battGauge * battCapacity) / 100   # 2.3.5  battCsum = ( battGauge * 47520) / 100

            if (battV <= 3.3):
                battCsum = battCapacity * 0.01 # 2.3.5   47520 * 0.01  # Charge set 1%
            battGauge = battCsum * 100 / battCapacity # 2.3.5  47520
            internalSettings['battery_charge'] = battCsum  # 5b  configurator.writeVRAM('battery_charge', battCsum)

else:
    def coldInit(): # 2.2.2b   skyhook version of coldInit()    from do_InitSolar()
        global BarSensOnline, DustSensOnline, AccSensOnline, IVSensOnline, TempBattOnLine, TempFitletOnLine
        global zeroAccX, zeroAccY, zeroAccZ

        print("coldInit for skyhook " )

        # Init the weather sensors
        BarSensOnline = BarSens.begin() # return True if chip found
        DustSensOnline = DustSens.begin() # return True if chip found

        if DustSensOnline: #  2.2.3b  power up dust sensor
            DustSens.powerMode(1)

        # Init / search for  accelerometer
        AccSensOnline = Acc.begin()  # return True if chip found
        if AccSensOnline == True:
            pitch = Acc.do_Pitch()
            zeroAccX, zeroAccY, zeroAccZ = Acc.x, Acc.y, Acc.z  # acq tilt sensor base line
            print('ZerrAcc: %r %r %r' % (zeroAccX, zeroAccY, zeroAccZ) )

        # Init pycom relay board
        IVSensOnline = 1
        if DIO.begin():
            skyhook_Relay(0,False)  # Open all the relays, no power...  read offset INA219
            time.sleep(1)
        else:
            IVSensOnline = 0  # Relay board has a problem with I2C comm
        # If any of sensors of the relay board is measing trigger False = 0
        IVSensOnline &= IVCh1.begin() # check presence and read offset
        IVSensOnline &= IVCh2.begin()
        IVSensOnline &= IVCh3.begin()
        IVSensOnline &= IVCh4.begin()
        #
        IVSensOnline &= IVCh5.begin()
        IVSensOnline &= IVCh6.begin()
        IVSensOnline &= IVCh7.begin()
        IVSensOnline &= IVCh8.begin()
        #
        if (IVSensOnline == 1):  # If all is good with power distribution
            file_KConfig(1)  #  read KConfig
            skyhook_Relays() #  manage relays schedule
            file_rgbConfig(1)  #  read and restore rgb led

        # Init battery temp monitor sensor
        # TempSensOnLine = 1
        # TempSensOnLine &= TempBatt.begin()
        # TempSensOnLine &= TempFitlet.begin()
        TempBattOnLine = TempBatt.begin()
        TempFitletOnLine = TempFitlet.begin()

        battV = deviceSettings['low_battery_voltage_threshold'] # if battery reading unavailable assume good



def changeFlashSetting(setting=None):
    global deviceSettings

    if setting == None:
        print("Please pass a device setting key to the function - eg: changeFlashSetting('device_id')")
        print(deviceSettings)
        return
    configurator.changeFlashSetting(setting,deviceSettings)
    deviceSettings= configurator.readFlashConfig()

def resetFlashSettings():
    global breakThread

    print('Pausing sampling loop thread...', endln='')
    breakThread = True
    time.sleep(4)
    print('paused.')
    time.sleep(1)
    wdt.init(30*60*1000) # 2.2.20  30min     1800000)
    disableAlive() # 2.2.20  1H
    previousSettings = deviceSettings.copy()
    print(previousSettings)
    configurator.resetFlashConfig(oldSettings=previousSettings)
    wdt.init(60*1000) # 2.2.20  60 sec
    disableAlive(1) # 2.2.21  2min
    IamAlive() # 2.2.20  resume DogWatch
    breakThread = false
    mainLoop(1)





def dprint(string, debugging=False, endln=None):
    global deviceSettings, sendConsole
    if sendConsole > 0 and mqttConnected:
        mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/console', msg=str(string), qos=MQTTqos)
        sendConsole = sendConsole - 1
    if 'debugging_messages' in deviceSettings:
        if deviceSettings['debugging_messages']:
            if endln == None:
                builtins.print(string)
            else:
                builtins.print(string, end=endln) #print all
            if (deviceSettings['ap_enabled']):
                for user in wss:
                    packetToSendWs = {}
                    #if 'console' in wss[user]['subscriptions']:
                    packetToSendWs['console'] = str(string)
                    if packetToSendWs:
                        wss[user]['ws'].SendTextMessage(jsonToStr(packetToSendWs))
        else:
            if not debugging:
                if endln == None:
                    builtins.print(string)
                else:
                    builtins.print(string,end=endln) #print all
                if (deviceSettings['ap_enabled']):
                    for user in wss:
                        packetToSendWs = {}
                        #if 'console' in wss[user]['subscriptions']:
                        packetToSendWs['console'] = str(string)
                        if packetToSendWs:
                            wss[user]['ws'].SendTextMessage(jsonToStr(packetToSendWs))
    else:
        if endln == None:
            builtins.print(string)
        else:
            builtins.print(string,end=endln) #print all
        if (deviceSettings['ap_enabled']):
            for user in wss:
                packetToSendWs = {}
                #if 'console' in wss[user]['subscriptions']:
                packetToSendWs['console'] = str(string)
                if packetToSendWs:
                    wss[user]['ws'].SendTextMessage(jsonToStr(packetToSendWs))

print = dprint
setupwebserver = None

wdt.init(60*1000)  # 60sec
IamAlive() # 2.2.20   wdt.feed()



# 2.2.1b
# compute SPEC gain factor
# Module H2S 50ppm 968-003 SPEC.pdf  page #3   'calculating gas concentration'
#            TIA = 49.9   sensitivity  150.15na/ppm
# 1/M =  1 / ( [15] * 49.9 *10-6) = 122.9754
def MFactor_SPEC (sensitivity, TIA):
    # 2.2.24b  return  1 / ( sensitivity * TIA * 0.000001)
    return  ( sensitivity * TIA * 0.000001)



# 2.2.1b  all channel report AVG value only
# 'pm10.0': { 'unit': 'ug/m^3', 'methods': ['AVG', 'MIN', 'MAX'] }

# 2.2.0b  add tags:  windSpeed, winDir, ir_c1, ir_co2, h2s
sensorRegistery = { 'cpu_temp': { 'unit': 'C', 'methods': ['AVG'] }, 'temperature': { 'unit': 'C', 'methods': ['AVG'] }, 'humidity': { 'unit': '%RH', 'methods': ['AVG'] }, \
 'pressure': { 'unit': 'Pa', 'methods': ['AVG'] } ,'pm1.0': { 'unit': 'ug/m^3', 'methods': ['AVG'] }, 'pm2.5': { 'unit': 'ug/m^3', 'methods': ['AVG'] }, \
 'pm10.0': { 'unit': 'ug/m^3', 'methods': ['AVG'] }, 'tvoc': { 'unit': 'ppb', 'methods': ['AVG'] }, 'pid_tvoc': { 'unit': 'ppb', 'methods': ['AVG'] }, 'co2e': { 'unit': 'ppm', 'methods': ['AVG'] }, \
 'instant_charge': { 'unit': 'c', 'methods': ['AVG'] }, 'battery_charge': { 'unit': 'c', 'methods': ['AVG'] }, 'battery_voltage': { 'unit': 'V', 'methods': ['AVG'] }, 'battery_gauge': { 'unit': '%', 'methods': ['AVG'] }, \
 'windSpeed': { 'unit': 'mph', 'methods': ['AVG'] }, 'winDir': { 'unit': 'degree', 'methods': ['AVG'] }, 'ir_c1': { 'unit': 'ppm', 'methods': ['AVG'] }, \
 'ir_co2': { 'unit': 'ppm', 'methods': ['AVG'] }, 'h2s': { 'unit': 'ppb', 'methods': ['AVG'] }, 'battery_crate': { 'unit': '%/hr', 'methods': ['AVG'] }, \
 'accel_x': { 'unit': 'g', 'methods': ['AVG'] }, 'accel_y': { 'unit': 'g', 'methods': ['AVG'] }, 'accel_z': { 'unit': 'g', 'methods': ['AVG'] }, 'accel_tilt': { 'unit': 'bool', 'methods': ['AVG'] }, \
 'IVCh1_ish': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh1_ubus': { 'unit': 'V', 'methods': ['AVG'] }, 'IVCh1_ipw': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh1_pw': { 'unit': 'W', 'methods': ['AVG'] }, \
 'IVCh2_ish': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh2_ubus': { 'unit': 'V', 'methods': ['AVG'] }, 'IVCh2_ipw': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh2_pw': { 'unit': 'W', 'methods': ['AVG'] }, \
 'IVCh3_ish': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh3_ubus': { 'unit': 'V', 'methods': ['AVG'] }, 'IVCh3_ipw': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh3_pw': { 'unit': 'W', 'methods': ['AVG'] }, \
 'IVCh4_ish': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh4_ubus': { 'unit': 'V', 'methods': ['AVG'] }, 'IVCh4_ipw': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh4_pw': { 'unit': 'W', 'methods': ['AVG'] }, \
 'IVCh5_ish': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh5_ubus': { 'unit': 'V', 'methods': ['AVG'] }, 'IVCh5_ipw': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh5_pw': { 'unit': 'W', 'methods': ['AVG'] }, \
 'IVCh6_ish': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh6_ubus': { 'unit': 'V', 'methods': ['AVG'] }, 'IVCh6_ipw': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh6_pw': { 'unit': 'W', 'methods': ['AVG'] }, \
 'IVCh7_ish': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh7_ubus': { 'unit': 'V', 'methods': ['AVG'] }, 'IVCh7_ipw': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh7_pw': { 'unit': 'W', 'methods': ['AVG'] }, \
 'IVCh8_ish': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh8_ubus': { 'unit': 'V', 'methods': ['AVG'] }, 'IVCh8_ipw': { 'unit': 'A', 'methods': ['AVG'] }, 'IVCh8_pw': { 'unit': 'W', 'methods': ['AVG'] }, \
 'temp_batt': { 'unit': 'F', 'methods': ['AVG'] }, 'temp_fitlet': { 'unit': 'F', 'methods': ['AVG'] }, 'mos_co': { 'unit': 'ppm', 'methods': ['AVG'] }, 'mos_no2': { 'unit': 'ppm', 'methods': ['AVG'] }, \
 'temp_ambient': { 'unit': 'F', 'methods': ['AVG'] }, 'pressure_atm': { 'unit': 'Pa', 'methods': ['AVG'] }, 'resp': { 'unit': 'ppm', 'methods': ['AVG'] }, 'mos_nh3': { 'unit': 'ppm', 'methods': ['AVG'] }, \
 'ozone': { 'unit': 'ppm', 'methods': ['AVG'] }, 'decibel': { 'unit': 'dB', 'methods': ['AVG'] }, 'so2': { 'unit': 'ppm', 'methods': ['AVG'] }, 'no2': { 'unit': 'ppm', 'methods': ['AVG'] }, 'co': { 'unit': 'ppm', 'methods': ['AVG'] }  } # 2.2.24b

sensorsPacket = {}

apTimer = Timer.Chrono()

def minimalNumber(x):
    # cf3/11 return float("{:.2f}".format(x))
    return float("{:.3f}".format(x))  # cf3/11


# cf3/11
# works with buildSamplePacket()
# use average value in volt to compute o3_concentration
# average is used for detection of alarm offset
def CheckSPECSensors(sensor, value):
    is_SPEC = False
    if sensor == "h2s":
        offset = internalSettings['h2s_offset']
        gain = internalSettings['h2s_gain']
        is_SPEC = True
    if sensor == "ozone":
        offset = internalSettings['o3_offset']
        gain = internalSettings['o3_gain']
        is_SPEC = True
    if sensor == "no2":
        offset = internalSettings['no2_offset']
        gain = internalSettings['no2_gain']
        is_SPEC = True
    if sensor == "co":
        offset = internalSettings['co_offset']
        gain = internalSettings['co_gain']
        is_SPEC = True
    if sensor == "resp":
        offset = internalSettings['resp_offset']
        gain = internalSettings['resp_gain']
        is_SPEC = True
    if sensor == 'so2':
        offset = internalSettings['so2_offset']
        gain = internalSettings['so2_gain']
        is_SPEC = True
    if is_SPEC:
        concentration =  gain * ( value - offset)
        if ( concentration < 0):
            if ( concentration < -1):
                print(sensor + " must be calibrated - Signal= " + str(value) + " Off= " + str(offset))
            concentration = 0
        if deviceSettings['debugging_messages']:
            print(sensor + " volt: " + str(value))
        value = concentration

    return value


def buildSamplePacket():
    global deviceSettings, sensorRegistery
    global sensorsPacket
    global loopCounter  # 3b

    buildPacket = {}
    buildPacket['timestamp'] = sensorsPacket['timestamp']
    buildPacket['loopcounter'] = {} # 3b
    buildPacket['loopcounter']['value'] = loopCounter  # 3b
    buildPacket['pingTime'] = currentPingTime
    buildPacket['packetLoss'] = { "value": currentPacketLoss, "unit": "percent" }

    # 8b  sent once with config packet
    # if abs(GPSAccuracy) < 0.001: # not(GPSLat == 0) and not(GPSLong == 0): # if l76Available == True:
    #    buildPacket['GPSLat'] = {}
    #    buildPacket['GPSLong'] = {}
    #    buildPacket['GPSLat']['value'] = GPSLat
    #    buildPacket['GPSLong']['value'] = GPSLong


    del sensorsPacket['timestamp']
    for sensor in sensorsPacket:
        buildPacket[sensor] = {}
        buildPacket[sensor]['unit'] = sensorRegistery[sensor]['unit']
        sensorValues = sensorsPacket[sensor]['values']
        for method in sensorRegistery[sensor]['methods']:
            if method == 'AVG':
                # cf3/10 buildPacket[sensor]['value'] = minimalNumber(sum(sensorValues)/len(sensorValues))
                value1 = minimalNumber(sum(sensorValues)/len(sensorValues)) # cf3/11
                buildPacket[sensor]['value'] = CheckSPECSensors(sensor, value1) # cf3/11
            if method == 'MAX':
                buildPacket[sensor]['max'] = minimalNumber(max(sensorValues))
            if method == 'MIN':
                buildPacket[sensor]['min'] = minimalNumber(min(sensorValues))
            if method == 'RAW':
                buildPacket[sensor]['values'] = sensorValues
    sensorsPacket = {}
    return buildPacket

def pushSensorValue(sensor, value):
    global sensorsPacket

    if sensor not in sensorsPacket:
        sensorsPacket[sensor] = { 'values': [] }
    if 'values' not in sensorsPacket[sensor]:
        sensorsPacket[sensor] = { 'values': [] }
    sensorsPacket[sensor]['values'].append(value)
    return

''' CF   already done in sampleSensors()
def checkBatteryVoltage():
    global deviceSettings, ads1115Available
    battV = deviceSettings['low_battery_voltage_threshold'] # if battery reading unavailable assume good?
    if ads1115Available:
        if deviceSettings['pycom_socket_version'] == 2: # 2.09
            battV = ads1115.get_voltage(ads1115.read(channel=1, gain=PGA_2_048V)) # 2.09
        else: # 2.09
            battV = ads1115.get_voltage(ads1115.read(channel=2, gain=PGA_2_048V))
        battV = battV * 220 / 100  #  https://ohmslawcalculator.com/voltage-divider-calculator
    else:
        battV = ReadBattV()
    return battV
'''

def testWeatherShieldAD():  # 2.2.4b  test weather shield ADC
    print( "Read the WeatherShield ADCs 100 times (Ctrl C to exit)")
    print( "!!! no IR, PID nor SPEC modules should be installed !!!")
    for x in range(100):
        time.sleep(2)

        # S1 = wsoIC3.get_voltage(wsoIC3.diff(chanPos=2, chanNeg=3, gain=PGA_1_024V))
        S1 = wsoIC3.get_voltage(wsoIC3.read(channel=2, gain=PGA_4_096V))
        S2 = wsoIC3.get_voltage(wsoIC3.read(channel=3, gain=PGA_4_096V))
        print(" WS_IC3 - IR U1 (Pin#5 Vout):" + str(S1) + " (GND=0V):" + str(S2))
        S1 = wsoIC3.get_voltage(wsoIC3.read(channel=0, gain=PGA_4_096V))
        print(" WS_IC3 - IR U2 (Pin#5 Vout):" + str(S1))
        time.sleep(0.1)

        S1 = wsoIC1.get_voltage(wsoIC1.read(channel=0, gain=PGA_4_096V))
        S2 = wsoIC1.get_voltage(wsoIC1.read(channel=1, gain=PGA_4_096V))
        print(" WS_IC1 - IR U8 (Pin#5 Vout):" + str(S1) + " (short J5, GND=0V):" + str(S2))
        time.sleep(0.1)

        S1 = wsoIC1.get_voltage(wsoIC1.read(channel=2, gain=PGA_4_096V))
        S2 = wsoIC1.get_voltage(wsoIC1.read(channel=3, gain=PGA_4_096V))
        print(" WS_IC1 - IR U5 (Pin#5 Vout):" + str(S1) + " (short J4, GND=0V):" + str(S2))
        time.sleep(0.1)

        S1 = wsoIC4.get_voltage(wsoIC4.read(channel=0, gain=PGA_4_096V))
        S2 = wsoIC4.get_voltage(wsoIC4.read(channel=1, gain=PGA_4_096V))
        print(" WS_IC4 - SPEC U7 (Pin#1 VGas):" + str(S1) + " (Pin#2 VRef):" + str(S2))
        time.sleep(0.1)

        S1 = wsoIC4.get_voltage(wsoIC4.read(channel=2, gain=PGA_4_096V))
        S2 = wsoIC4.get_voltage(wsoIC4.read(channel=3, gain=PGA_4_096V))
        print(" WS_IC4 - SPEC U6 (Pin#1 VGas):" + str(S1) + " (Pin#2 VRef):" + str(S2))
        time.sleep(0.1)



def tstWind(): # 2.2.0b
    print( "Read the wind sensor 100 times (Ctrl C to exit)")
    for x in range(100):
        # measure wind speed in mph
        chrono.reset() # for improved resolution of time period use chrono
        chrono.start()
        windCount1 = wsoIC6.readPort()
        time.sleep(2)
        windCount2 = wsoIC6.readPort()
        chrono.stop()
        windSampling = chrono.read_ms() # measure period in ms
        print("Wind speed (ms):" + str(windSampling) + " Cpt1:" + str(windCount1) + " Cpt2:" + str(windCount2))
        windCount = windCount2 - windCount1
        if windCount1 > windCount2:
            windCount = 255 - windCount1
            windCount += windCount2
        windSpeed =  windCount * ( 2.25 / (windSampling / 1000) )    # V = P(2.25/T)
        print("windSpeed (mph):" + str(windSpeed) + " Cpt:" + str(windCount))

        # measure wind direction in degre from 0 to 360
        # resolution from spec 22.5 degre
        # 180 degre is south - 0 = 360 is north
        S1 = wsoIC3.get_voltage(wsoIC3.read(channel=1, gain=PGA_4_096V))
        # 3.3V = 359 degre
        # S1 = n degre
        if S1 > 3.3:  S1 = 3.3
        winDir = int ( (S1 * 359) / 3.3)
        print("winDir (V):" + str(S1) + " angle:" + str(winDir) )


# 2.2.12b
# calibrate CO2 @ 400 ppm
def calibCO2():
    print('Wait 30min to warmup sensor, execute writeFram() to save the calibration') # cf3/10
    S1 = wsoIC1.get_voltage(wsoIC1.diff(chanPos=2, chanNeg=3, gain=PGA_2_048V))
    print ("V_CO2 @ 400ppm:" + str(S1))
    configurator.writeVRAM('co2_offset', S1)
    internalSettings['co2_offset'] = S1   # V @ 407ppm


# 2.2.12b
# calibrate C1 @ 0 ppm
def calibC1():
    print('Wait 30min to warmup sensor, execute writeFram() to save the calibration') # cf3/10
    S1 = wsoIC3.get_voltage(wsoIC3.read(channel=0, gain=PGA_2_048V))
    print ("V_C1 @ 0ppm:" + str(S1))
    configurator.writeVRAM('c1_offset', S1)
    internalSettings['c1_offset'] = S1   # V @ 0ppm


# 2.2.12b
# after calibrating the zero (400ppm) CO2,
# use this function to compute ppm from actual Vout of the sensor
# assumes that the sensor gives 5% for 2V

# 2.2.24    co2_range = 300000 # 2.2.23b   ppm 50000 = 5%
def computeCO2(S1):
    #  0.098V  =    407ppm
    #  2V      =    50000ppm
    # y = 26077.81*x - 2155.626
    # https://www.statisticshowto.com/probability-and-statistics/regression-analysis/find-a-linear-regression-equation/
    #
    # S1 = wsoIC1.get_voltage(wsoIC1.diff(chanPos=2, chanNeg=3, gain=PGA_2_048V))
    co2_range = internalSettings['co2_range'] # 2.2.24b   ppm 50000 = 5%
    avgX = (internalSettings['co2_offset'] + 2) / 2
    # print ("avgX:" + str(avgX))
    avgY = (407 + co2_range) / 2 # 2.2.23b   avgY = (407 + 50000) / 2
    # print ("avgY:" + str(avgY))
    ssX = (avgX - internalSettings['co2_offset']) ** 2
    ssX = ssX + ((avgX - 2) ** 2)
    # print ("ssX:" + str(ssX))
    ssY = (avgX - internalSettings['co2_offset']) * ( avgY - 407)
    ssY = ssY + ( (avgX - 2) * ( avgY - co2_range) )  # 2.2.23b  ssY = ssY + ( (avgX - 2) * ( avgY - 50000) )
    # print ("ssY:" + str(ssY))
    slope = ssY / ssX
    # print ("slope:" + str(slope))
    intercept = avgY - ( slope * avgX)
    # print ("intercept:" + str(intercept))
    # print ("CO2 equ: " + str(slope) + "x + " + str(intercept) )
    Y = (slope * S1) + intercept
    if Y > co2_range: Y = co2_range  # 2.2.23b   if Y > 50000: Y = 50000
    if Y < 0:  Y = 0
    return Y

# 2.2.12b
def computeC1(S1):
    #  0.098V  =    0ppm
    #  2V      =    50000ppm
    # y = 26288.12*x - 2576.236
    #
    # S1 = wsoIC3.get_voltage(wsoIC3.read(channel=0, gain=PGA_2_048V))
    c1_range = internalSettings['c1_range'] # 2.2.24b   ppm 50000 = 5%
    avgX = (internalSettings['c1_offset'] + 2) / 2
    avgY = (0 + c1_range) / 2   # 2.2.24   50000) / 2
    ssX = (avgX - internalSettings['c1_offset']) ** 2
    ssX = ssX + ((avgX - 2) ** 2)
    ssY = (avgX - internalSettings['c1_offset']) * ( avgY - 0)
    ssY = ssY + ( (avgX - 2) * ( avgY - c1_range) )  #  2.2.24b   50000) )
    slope = ssY / ssX
    intercept = avgY - ( slope * avgX)
    # print ("C1 equ: " + str(slope) + "x + " + str(intercept) )
    Y = (slope * S1) + intercept
    if Y > c1_range: Y = c1_range   # 2.2.24b  50000: Y = 50000
    if Y < 0:  Y = 0
    return Y



# 2.3.4 ADC root declaration to SPEC sensor
#       U4= IC1_0= 0  //   U3= IC1_2= 1  //  U7= IC4_0= 2  //   U6= IC4_2= 3
def SPEC_read(root):
    ain = 0
    if (root == 0) or (root == 1):
        if root == 1: ain = 2
        SGas = wsoIC1.get_voltage(wsoIC1.read(channel=ain, gain=PGA_2_048V))
        # SGas = wsoIC1.get_voltage(wsoIC1.read(channel=ain, gain=PGA_2_048V)) # cf3/8
    if (root == 2) or (root == 3):
        if root == 3: ain = 2
        SGas = wsoIC4.get_voltage(wsoIC4.read(channel=ain, gain=PGA_2_048V))
        # SGas = wsoIC4.get_voltage(wsoIC4.read(channel=ain, gain=PGA_2_048V)) # cf3/8
    return SGas



# 2.2.24b
# configurator.writeFlashKey('weather_shield_option', 159 + 0b0100000 + 0b01000000) # b5=O3  b6=dB
o3_concentration = 0  # cf3/8
o3_volt = 0 # cf3/8   128smp/sec

def ozone_read():
    global o3_concentration # cf3/8
    global o3_volt # cf3/8
    global samplingSequence # cf3/9

    if (samplingSequence & 2) == 2: # cf3/9
        if (deviceSettings['weather_shield_option'] & 0b0100000) != 0: #  b5 = O3
            '''
            o3_offset = internalSettings['o3_offset']
            o3_gain = internalSettings['o3_gain']
            o3_root = internalSettings['o3_root']
            o3_volt = SPEC_read(o3_root)  # 2.3.4  wsoIC1.get_voltage(wsoIC1.read(channel=0, gain=PGA_2_048V)) # U4
            # 2.2.24b  SRef = wsoIC1.get_voltage(wsoIC1.read(channel=1, gain=PGA_2_048V))
            # pushSensorValue('ozone', SGas)  # for debug only push V reading
            # print('Gas:' + str(SGas) + ' Ref:' + str(SRef) + ' Diff:' + str(SGas - SRef))
            SGas0 = o3_offset # 2.2.24b  # SGas0 = SRef + o3_offset
            o3_concentration =  o3_gain * ( o3_volt - SGas0) # expect  0 to 20ppm
            if (o3_concentration < 0):
                if ( o3_concentration < -1):
                    # 2.3.11 file_errLog(0, "O3 sensor must be calibrated - SGas= " + str(SGas) + " Off= " + str(SGas0), sdAvailable)
                    print("O3 sensor must be calibrated - SGas= " + str(o3_volt) + " Off= " + str(SGas0)) # 2.3.11
                o3_concentration = 0
            # print('Ozone:' + str(o3_concentration) + ' VGas:' + str(SGas) + ' VRef:' + str(SRef))
            pushSensorValue('ozone', o3_concentration)
            '''
            # cf3/11  store value in volt, so average in volt can be used for alarm offset
            #         see CheckSPECSensors()
            o3_root = internalSettings['o3_root']
            o3_volt = SPEC_read(o3_root)
            pushSensorValue('ozone', o3_volt)

# 2.2.24b
# internalSettings['o3_offset']   1.391521
# internalSettings['o3_gain']   -46.1434
def calibO3(): # calib zero
    # cf3/11 print('Did you wait 30min for the sensor to warmup before running calibration?')  # 2.3.2
    print('Wait 30min to warmup sensor, execute writeFram() to save the calibration') # cf3/11
    o3_root = internalSettings['o3_root']
    SGas = SPEC_read(o3_root) # 2.3.4   SGas = wsoIC1.get_voltage(wsoIC1.read(channel=0, gain=PGA_2_048V)) # U4
    SRef = 0; # 2.2.24b  wsoIC1.get_voltage(wsoIC1.read(channel=1, gain=PGA_2_048V))
    o3_offset = SGas - SRef
    print('O3 VOffset:' + str(o3_offset)) # 2.2.24b  + ' VGas:' + str(SGas) + ' VRef:' + str(SRef) )
    configurator.writeVRAM('o3_offset', o3_offset)
    internalSettings['o3_offset'] = o3_offset


# 2.2.24b  setO3Gain(-43.43)
def setO3Gain(sensitivty): # -43.43na/ppm    - execute this cmd each time a new sensor is installed
    M = MFactor_SPEC(sensitivty, 499)
    o3_gain = 1 / M
    print ("M Ozone:" + str(M) + "V/ppm (-15/-30/-45 mV/ppm) - Gain:" + str(o3_gain))
    configurator.writeVRAM('o3_gain', o3_gain)
    internalSettings['o3_gain'] = o3_gain

co_concentration = 0  # cf3/8
co_volt = 0 # cf3/8   128smp/sec

def co_read():
    global co_concentration # cf3/8
    global co_volt # cf3/8
    global samplingSequence # cf3/9

    if (samplingSequence & 64) == 64: # cf3/9
        if (deviceSettings['weather_shield_option'] & 0b0100000000000) != 0:
            co_root = internalSettings['co_root']
            co_volt = SPEC_read(co_root)
            pushSensorValue('co', co_volt)

def calibCO(): # calib zero
    # cf3/11 print('Did you wait 30min for the sensor to warmup before running calibration?')  # 2.3.2
    print('Wait 30min to warmup sensor, execute writeFram() to save the calibration') # cf3/11
    co_root = internalSettings['co_root']
    SGas = SPEC_read(co_root) # 2.3.4   SGas = wsoIC1.get_voltage(wsoIC1.read(channel=0, gain=PGA_2_048V)) # U4
    SRef = 0; # 2.2.24b  wsoIC1.get_voltage(wsoIC1.read(channel=1, gain=PGA_2_048V))
    co_offset = SGas - SRef
    print('CO VOffset:' + str(co_offset)) # 2.2.24b  + ' VGas:' + str(SGas) + ' VRef:' + str(SRef) )
    configurator.writeVRAM('co_offset', co_offset)
    internalSettings['co_offset'] = co_offset


# 2.2.24b  setO3Gain(-43.43)
def setCOGain(sensitivty): # -43.43na/ppm    - execute this cmd each time a new sensor is installed
    M = MFactor_SPEC(sensitivty, 100)
    co_gain = 1 / M
    print ("M CO:" + str(M) + "V/ppm (-15/-30/-45 mV/ppm) - Gain:" + str(co_gain))
    configurator.writeVRAM('co_gain', co_gain)
    internalSettings['co_gain'] = co_gain

no2_concentration = 0  # cf3/8
no2_volt = 0 # cf3/8   128smp/sec

def no2_read():
    global no2_concentration # cf3/8
    global no2_volt # cf3/8
    global samplingSequence # cf3/9

    if (samplingSequence & 128) == 128: # cf3/9
        if (deviceSettings['weather_shield_option'] & 0b01000000000000) != 0:
            no2_root = internalSettings['no2_root']
            no2_volt = SPEC_read(no2_root)
            pushSensorValue('no2', no2_volt)

def calibNO2(): # calib zero
    # cf3/11 print('Did you wait 30min for the sensor to warmup before running calibration?')  # 2.3.2
    print('Wait 30min to warmup sensor, execute writeFram() to save the calibration') # cf3/11
    co_root = internalSettings['no2_root']
    SGas = SPEC_read(no2_root) # 2.3.4   SGas = wsoIC1.get_voltage(wsoIC1.read(channel=0, gain=PGA_2_048V)) # U4
    SRef = 0; # 2.2.24b  wsoIC1.get_voltage(wsoIC1.read(channel=1, gain=PGA_2_048V))
    no2_offset = SGas - SRef
    print('CO VOffset:' + str(no2_offset)) # 2.2.24b  + ' VGas:' + str(SGas) + ' VRef:' + str(SRef) )
    configurator.writeVRAM('no2_offset', no2_offset)
    internalSettings['no2_offset'] = no2_offset


# 2.2.24b  setO3Gain(-43.43)
def setNO2Gain(sensitivty): # -43.43na/ppm    - execute this cmd each time a new sensor is installed
    M = MFactor_SPEC(sensitivty, 499)
    no2_gain = 1 / M
    print ("M NO2:" + str(M) + "V/ppm (-15/-30/-45 mV/ppm) - Gain:" + str(no2_gain))
    configurator.writeVRAM('no2_gain', no2_gain)
    internalSettings['no2_gain'] = no2_gain

# 2.3.1  RESP sensor 110901
# internalSettings['resp_offset']   1.316708
# internalSettings['resp_gain']  -44.2093
def calibRESP(): # calib zero
    # cf3/10 print('Did you wait 30min for the sensor to warmup before running calibration?')  # 2.3.2
    print('Wait 30min to warmup sensor, execute writeFram() to save the calibration') # cf3/10
    resp_root = internalSettings['resp_root']
    SGas = SPEC_read(resp_root) # 2.3.4  SGas = wsoIC4.get_voltage(wsoIC4.read(channel=2, gain=PGA_2_048V))
    SRef = 0;
    resp_offset = SGas - SRef
    print('RESP VOffset:' + str(resp_offset))
    configurator.writeVRAM('resp_offset', resp_offset)
    internalSettings['resp_offset'] = resp_offset


# 2.3.1  setRESPGain(-45.33)   -50 +/- 25 nA/ppm
def setRESPGain(sensitivty): # -45.33na/ppm    - execute this cmd each time a new sensor is installed
    M = MFactor_SPEC(sensitivty, 499)
    resp_gain = 1 / M
    print ("M RESP:" + str(M) + "V/ppm (-12.5/-20/-27.5 mV/ppm) - Gain:" + str(resp_gain))
    configurator.writeVRAM('resp_gain', resp_gain)
    internalSettings['resp_gain'] = resp_gain



# 2.3.1
resp_concentration = 0  # cf3/8
resp_volt = 0 # cf3/8   128smp/sec

def resp_read():
    global resp_concentration  # cf3/8
    global resp_volt # cf3/8   128smp/sec
    global samplingSequence # cf3/9

    if (samplingSequence & 8) == 8: # cf3/9
        if (deviceSettings['weather_shield_option'] & 0b0100000000) != 0: #  b8 = RESP
            ''' cf3/11
            resp_offset = internalSettings['resp_offset']
            resp_gain = internalSettings['resp_gain']
            resp_root = internalSettings['resp_root']
            resp_volt = SPEC_read(resp_root) # 2.3.4  SGas = wsoIC4.get_voltage(wsoIC4.read(channel=2, gain=PGA_2_048V))
            SGas0 = resp_offset
            resp_concentration =  resp_gain * ( resp_volt - SGas0) # expect  0 to 20ppm
            if (resp_concentration < 0):
                if ( resp_concentration < -1):
                    # 2.3.11 file_errLog(0, "RESP sensor must be calibrated - SGas= " + str(SGas) + " Off= " + str(SGas0), sdAvailable)
                    print("RESP sensor must be calibrated - SGas= " + str(resp_volt) + " Off= " + str(SGas0)) # 2.3.11
                resp_concentration = 0
            # print('RESP:' + str(resp_concentration) + ' VGas:' + str(SGas) + ' VRef:' + str(SRef))
            pushSensorValue('resp', resp_concentration)
            '''
            # cf3/11  store value in volt, so average in volt can be used for alarm offset
            #         see CheckSPECSensors()
            resp_root = internalSettings['resp_root']
            resp_volt = SPEC_read(resp_root)
            pushSensorValue('resp', resp_volt)



# 2.3.3  setSO2gain(15.20) // 28.58
def setSO2gain(sensitivty):  # 15.20 na/ppm   - execute each time a new sensor is installed
    M = MFactor_SPEC(sensitivty, 100.0)
    so2_gain = 1 / M
    print ("M SO2: " + str(M) + "V/ppm (2.5/3.0/3.5 mV/ppm) - Gain:" + str(so2_gain) )  # Vgas Span (M)
    configurator.writeVRAM('so2_gain', so2_gain)
    internalSettings['so2_gain'] = so2_gain


# 2.3.3  SO2 sensor 110601
# internalSettings['so2_offset']
# internalSettings['so2_gain']
def calibSO2(): # calib zero
    # cf3/10 print('Did you wait 30min for the sensor to warmup before running calibration?')
    print('Wait 30min to warmup sensor, execute writeFram() to save the calibration') # cf3/10
    so2_root = internalSettings['so2_root']
    SGas = SPEC_read(so2_root)  # 2.3.4  SGas = wsoIC4.get_voltage(wsoIC4.read(channel=2, gain=PGA_2_048V))
    SRef = 0;
    so2_offset = SGas - SRef
    print('SO2 VOffset:' + str(so2_offset))
    configurator.writeVRAM('so2_offset', so2_offset)
    internalSettings['so2_offset'] = so2_offset



# 2.3.3
so2_concentration = 0  # cf3/8
so2_volt = 0 # cf3/8   128smp/sec

def so2_read():
    global so2_concentration # cf3/8
    global so2_volt
    global samplingSequence # cf3/9

    if (samplingSequence & 16) == 16: # cf3/9
        if (deviceSettings['weather_shield_option'] & 0b010000000000) != 0: #  b10 = SO2
            ''' cf3/11
            so2_offset = internalSettings['so2_offset']
            so2_gain = internalSettings['so2_gain']
            so2_root = internalSettings['so2_root']
            so2_volt = SPEC_read(so2_root) # 2.3.4  SGas = wsoIC4.get_voltage(wsoIC4.read(channel=2, gain=PGA_2_048V))
            SGas0 = so2_offset
            so2_concentration =  so2_gain * ( so2_volt - SGas0) # expect  0 to 20ppm
            if (so2_concentration < 0):
                if ( so2_concentration < -1):
                    # 2.3.11 file_errLog(0, "SO2 sensor must be calibrated - SGas= " + str(SGas) + " Off= " + str(SGas0), sdAvailable)
                    print("SO2 sensor must be calibrated - SGas= " + str(so2_volt) + " Off= " + str(SGas0)) # 2.3.11
                so2_concentration = 0
            # print('SO2:' + str(so2_concentration) + ' VGas:' + str(SGas) + ' VRef:' + str(SRef))
            pushSensorValue('so2', so2_concentration)
            '''
            # cf3/11  store value in volt, so average in volt can be used for alarm offset
            #         see CheckSPECSensors()
            so2_root = internalSettings['so2_root']
            so2_volt = SPEC_read(so2_root)
            pushSensorValue('so2', so2_volt)


# 2.2.24b
h2s_concentration = 0  # cf3/8
h2s_volt = 0 # cf3/8   128smp/sec

def h2s_read():
    global h2s_concentration  # cf3/8
    global h2s_volt # cf3/8
    global samplingSequence # cf3/9

    if (samplingSequence & 1) == 1: # cf3/9
        if (deviceSettings['weather_shield_option'] & 8) != 0: #  b3 = H2S
            ''' cf3/11
            h2s_offset = internalSettings['h2s_offset']
            h2s_gain = internalSettings['h2s_gain']
            h2s_root = internalSettings['h2s_root']
            h2s_volt = SPEC_read(h2s_root)  # 2.3.4  SGas = wsoIC4.get_voltage(wsoIC4.read(channel=0, gain=PGA_2_048V)) # U7
            # cf3/11  h2s_volt = SPEC_read(h2s_root) # cf3/8
            # 2.2.24b  SRef = wsoIC4.get_voltage(wsoIC4.read(channel=1, gain=PGA_2_048V))
            # pushSensorValue('h2s', SGas) # for debug only push V reading
            SGas0 = h2s_offset # 2.2.24b   SGas0 = SRef + h2s_offset
            h2s_concentration =  h2s_gain * ( h2s_volt - SGas0) # expect  0 to 50ppm
            if (h2s_concentration < 0):
                if ( h2s_concentration < -1):
                    # 2.3.11 file_errLog(0, "H2S sensor must be calibrated - SGas= " + str(SGas) + " Off= " + str(SGas0), sdAvailable)
                    print("H2S sensor must be calibrated - SGas= " + str(h2s_volt) + " Off= " + str(SGas0)) # 2.3.11
                h2s_concentration = 0
            # print('H2S:' + str(h2s_concentration) + ' VGas:' + str(SGas) + ' VRef:' + str(SRef))
            pushSensorValue('h2s', h2s_concentration)
            '''
            # cf3/11  store value in volt, so average in volt can be used for alarm offset
            #         see CheckSPECSensors()
            h2s_root = internalSettings['h2s_root']
            h2s_volt = SPEC_read(h2s_root)
            pushSensorValue('h2s', h2s_volt)


# 2.2.24b
# internalSettings['h2s_offset']  1.407022
# internalSettings['h2s_gain']  133.4671
def calibH2S():  # calib zero
    print('Wait 30min to warmup sensor, execute writeFram() to save the calibration')  # cf3/11   2.3.2
    h2s_root = internalSettings['h2s_root']
    SGas = SPEC_read(h2s_root) # 2.3.4  SGas = wsoIC4.get_voltage(wsoIC4.read(channel=0, gain=PGA_2_048V)) # U7
    SRef = 0; # 2.2.24b  wsoIC4.get_voltage(wsoIC4.read(channel=1, gain=PGA_2_048V))
    h2s_offset = SGas - SRef
    print('H2S VOffset:' + str(h2s_offset) ) # 2.2.24b  + ' VGas:' + str(SGas) + ' VRef:' + str(SRef) )
    # print ("h2s_offset @ 0ppm: " + str(h2s_offset) + "V") # 1.514836
    configurator.writeVRAM('h2s_offset', h2s_offset)
    internalSettings['h2s_offset'] = h2s_offset


# 2.2.24b  setH2Sgain(150.15)
def setH2Sgain(sensitivty):  # 150.15 na/ppm   - execute each time a new sensor is installed
    M = MFactor_SPEC(sensitivty, 49.9)
    h2s_gain = 1 / M
    print ("M H2S: " + str(M) + "V/ppm (6.25/10.6/15 mV/ppm) - Gain:" + str(h2s_gain) )  # Vgas Span (M)
    configurator.writeVRAM('h2s_gain', h2s_gain)
    internalSettings['h2s_gain'] = h2s_gain




# https://circuitdigest.com/microcontroller-projects/arduino-sound-level-measurement
def decibel_read(): # 2.2.24b
    global samplingSequence # cf3/9

    if (samplingSequence & 4) == 4: # cf3/9
        if (deviceSettings['weather_shield_option'] & 0b01000000) != 0: #  b6 = dB
            decibel = wsoIC4.get_voltage(wsoIC4.read(channel=2, gain=PGA_2_048V)) # J6
            pushSensorValue('decibel', decibel)


# 2.3.2
# see curves from document 'MOS  MICS-6814 SGX.pdf'
# https://mycurvefit.com/     power curve fitting
# ratio= rs/r0   x= ratio  y= ppm
#
# CO ppm
# x,y\3.5,1\2,2\1,4.5\0.5,10\0.1,70\0.07,100\0.01,1000
# y = 4.51885*x^-1.172479
# CO = pow(ratio_RED, -1.172479) * 4.51885
#
# NO2 ppm
# x,y\0.065,0.01\0.1,0.015\0.65,0.1\1,0.15\6.5,1\10,1.5\20,3\30,4.5
# y = 0.1532128*x^0.9935413
# NO2 = pow(ratio_OX, 0.9935413) * 0.1532128
#
# NH3 ppm
# x,y\0.78,1\0.5,2.3\0.3,6\0.1,48\0.08,80\0.07,90\0.055,160
# y = 0.4656564*x^-2.009362
# NH3 = pow(ratio_NH3, -2.009362) * 0.46565
#
def mos6814_read(trace=False):
    global samplingSequence # cf3/9

    if (samplingSequence & 32) == 32: # cf3/9
        if (deviceSettings['weather_shield_option'] & 0b01000000000) != 0: #  b9 = 6814  RED, OX, NH3
            if adc6814Available:
                red_r0 = internalSettings['red_r0']
                red_rs = adc6814.read_RED()
                ratio_RED = red_rs / red_r0
                mos_co = pow(ratio_RED, -1.172479) * 4.51885
                if trace: print("mos_co: {}".format(mos_co))
                pushSensorValue('mos_co', mos_co)

                ox_r0 = internalSettings['ox_r0']
                ox_rs = adc6814.read_OX()
                ratio_OX = ox_rs / ox_r0
                mos_no2 = pow(ratio_OX, 0.9935413) * 0.1532128
                if trace: print("mos_no2: {}".format(mos_no2))
                pushSensorValue('mos_no2', mos_no2)

                nh3_r0 = internalSettings['nh3_r0']
                nh3_rs = adc6814.read_NH3()
                ratio_NH3 = nh3_rs / nh3_r0
                mos_nh3 = pow(ratio_NH3, -2.009362) * 0.46565
                if trace: print("mos_nh3: {}".format(mos_nh3))
                pushSensorValue('mos_nh3', mos_nh3)


# 2.3.2
def calibMOS6814(): # wait 30min for the sensor to warmup before running calibration
    if (deviceSettings['weather_shield_option'] & 0b01000000000) != 0: #  b9 = 6814  RED, OX, NH3
        if adc6814Available:
            # cf3/10  print('Did you wait 30min for the sensor to warmup before running calibration?')
            print('Wait 30min to warmup sensor, execute writeFram() to save the calibration') # cf3/10
            red_r0 = adc6814.read_RED()  # valid 100k  to 1500K
            print("red_r0: {}".format(red_r0))
            configurator.writeVRAM('red_r0', red_r0)
            internalSettings['red_r0'] = red_r0

            ox_r0 = adc6814.read_OX()  # valid 0.8K  to 20K
            print("ox_r0: {}".format(ox_r0))
            configurator.writeVRAM('ox_r0', ox_r0)
            internalSettings['ox_r0'] = ox_r0

            nh3_r0 = adc6814.read_NH3() # valid 10k  to 1500k
            print("nh3_r0: {}".format(nh3_r0))
            configurator.writeVRAM('nh3_r0', nh3_r0)
            internalSettings['nh3_r0'] = nh3_r0




S1_ir = 0 # 2.2.12b   CO2  in volt
S2_ir = 0 # 2.2.21   C1 in volt
temperature = 0 # 2.2.21  preserve for correction of VOC
pressure = 0
humidity = 0

samplingSequence = 255 # cf3/9   Set to 1 to start sequencing reading to SPEC sensors
pauseSampling = False  # cf3/9

if Skyhook == False:  # 2.2.2b
    def sampleSensors(msg="Sampling...\n"):
        global time_dust, battV, battI # CF
        global deviceSettings, sensorsPacket, nvsKeys, internalSettings
        global bme280Available, hm3300Available, ads1115Available, mpsAvailable
        global time_wind, wsoAvailable, wsOptionAvailable # 2.2.20    # 2.2.0b
        global time_fuel, fuelgaugeAvailable # 2.2.1b
        global time_sgp30, sgp30Available # 2.2.1b
        global sdAvailable # 2.2.10b
        global S1_ir, S2_ir # 2.2.12b  for debug
        global temperature, pressure, humidity # 2.2.21
        global sgp30TempThreshold, sgp30HumidityThreshold  # cf3/7
        global samplingSequence,failedSensors # cf3/9

        # 2.2.1b
        chrono.reset()
        chrono.start()
        try: # 2.2.9b

            if 'timestamp' not in sensorsPacket:
                sensorsPacket['timestamp'] = time.time()

            pushSensorValue('cpu_temp', (machine.temperature() - 32) / 1.8) # processor temperature in °C

            if pauseSampling == True: # cf3/9
                chrono.stop()
                return chrono.read_ms()

            #SPEC_read(internalSettings['h2s_root'])  # cf3/11
            if wsOptionAvailable == True: # 2.3.8

                # 2.2.0b
                # deviceSettings['weather_shield_option'] = 1 # for debug only
                # 2.2.23b  for compatibility with WS VC3
                # if wsOptionAvailable: # 2.2.20  if wsoAvailable:
                #
                # print( "Read optional sensors .")
                # b4 = Wind speed IC6 $20
                #      Wind dir   IC3 $49 AIN_1
                # min and resolution 1.125mph => 0.5m/s     max 199mph = 177 cpt => 90m/s
                # it takes 2sec per reading, to increase resolution, increase sampling time
                if (deviceSettings['weather_shield_option'] & 16) != 0: #  wind sensors ?
                    time_wind -= 1
                    if time_wind <= 0:  # cycle the reading of wind sensor, it takes 2sec per reading
                        time_wind = 60  # one measure every 60 sec

                        # measure wind speed in mph
                        # chrono.reset() # for improved resolution of time period use chrono
                        # chrono.start()
                        windCount1 = wsoIC6.readPort()
                        time.sleep(2)
                        windCount2 = wsoIC6.readPort()
                        # chrono.stop()
                        windSampling = 2000 # windSampling = chrono.read_ms() # measure period in ms
                        # print("Wind speed (ms):" + str(windSampling) + " Cpt1:" + str(windCount1) + " Cpt2:" + str(windCount2))
                        windCount = windCount2 - windCount1
                        if windCount1 > windCount2:
                            windCount = 255 - windCount1
                            windCount += windCount2
                        windSpeed =  windCount * ( 2.25 / (windSampling / 1000) )    # V = P(2.25/T)
                        print("windSpeed (mph):" + str(windSpeed) + " Cpt:" + str(windCount))
                        pushSensorValue('windSpeed', windSpeed)

                        # measure wind direction in degre from 0 to 360
                        # resolution from spec 22.5 degre
                        # 180 degre is south - 0 = 360 is north
                        S1 = wsoIC3.get_voltage(wsoIC3.read(channel=1, gain=PGA_4_096V))
                        # 3.3V = 359 degre
                        # S1 = n degre
                        if S1 > 3.3: S1 = 3.3
                        winDir = int( (S1 * 359) / 3.3)
                        print("winDir (V):" + str(S1) + " angle:" + str(winDir) )
                        pushSensorValue('winDir', winDir)

                if deviceSettings['pycom_socket_version'] == 3 or deviceSettings['pycom_socket_version'] == 21 or deviceSettings['pycom_socket_version'] == 4: # cf3/23   2.2.12b   2.2.6b
                    # b1 = ir_c1 U2  IC3 $49  AIN_0  # U5  IC1 $4B  AIN_2P-3N
                    if (deviceSettings['weather_shield_option'] & 2) != 0: # IR CH4 = C1 = methane ?
                        # S1 = wsoIC1.get_voltage(wsoIC1.diff(chanPos=2, chanNeg=3, gain=PGA_4_096V))  # 2.4V max
                        S1 = wsoIC3.get_voltage(wsoIC3.read(channel=0, gain=PGA_2_048V))
                        S2_ir = S1  # C1 in Volt
                        # 2.2.12b  S1 = S1 - internalSettings['c1_offset']
                        #          if S1 < 0: S1 = 0
                        #          ir_c1 = S1 * internalSettings['c1_gain']
                        ir_c1 = computeC1(S1) # 2.2.12b
                        # print("3VO IR_C1: %d  S1: %f" % (ir_c1, S1)) # 2.2.12b
                        pushSensorValue('ir_c1', ir_c1)


                    # b0 = IR_CO2 U5  IC1 $4B  AIN_2P-3N   # U2  IC3 $49  AIN_0        # Vb  IR_CO2 U8  IC1 $4B  AIN_0P-1N
                    if (deviceSettings['weather_shield_option'] & 1) != 0: #  IR CO2 ?
                        # vb S1 = wsoIC1.get_voltage(wsoIC1.diff(chanPos=0, chanNeg=1, gain=PGA_1_024V))
                        # S1 = wsoIC3.get_voltage(wsoIC3.read(channel=0, gain=PGA_4_096V)) # Va  2.4V max
                        S1 = wsoIC1.get_voltage(wsoIC1.diff(chanPos=2, chanNeg=3, gain=PGA_2_048V))
                        S1_ir = S1  # CO2 in Volt
                        # 2.2.12b  S1 = S1 - internalSettings['co2_offset']
                        #          if S1 < 0: S1 = 0
                        #          ir_co2 = S1 * internalSettings['co2_gain']
                        ir_co2 = computeCO2(S1) # 2.2.12b
                        # print("3VO IR_CO2: %d  S1: %f" % (ir_co2, S1)) # 2.2.12b
                        pushSensorValue('ir_co2', ir_co2)

                ''' 2.2.24
                # b3 = H2S U7     IC4 $4A  AIN_0P-1N       SPEC sensor
                # 0-50ppm     (6.25min 10.6typ 15max mV/ppm)
                # Cppm = 1/M * (Vgas)
                # M = sensitivity code [96] * Gain * 10-6
                if (deviceSettings['weather_shield_option'] & 8) != 0: #  H2S sensor ?
                    # S1 = wsoIC4.get_voltage(wsoIC4.diff(chanPos=0, chanNeg=1, gain=PGA_2_048V))  # 3V max
                    SP = wsoIC4.get_voltage(wsoIC4.read(channel=0, gain=PGA_2_048V)) # P
                    SN = wsoIC4.get_voltage(wsoIC4.read(channel=1, gain=PGA_2_048V)) # N
                    # Init offset:
                    # P  -  N  -  offset - result
                    # 160  150    10
                    # Measure no gas:
                    # 160 - (150 + 10)      0
                    # temperature offset + 10:
                    # 170 - (160 + 10)      0
                    # gas 10 units:
                    # 170 - (150 + 10)      10
                    S1 = SP - (SN + internalSettings['h2s_offset']) # offset = SP - SN   @ 0 gas
                    if S1 < 0: S1 = 0
                    h2s = S1 * internalSettings['h2s_gain']  # to compute gain, use   MFactor_SPEC(sensitivity, TIA)
                    # print("H2S " + str(h2s) + "ppm   SP:" + str(SP) + " SN:" + str(SN) + " S1:" + str(S1))
                    pushSensorValue('h2s', h2s)
                '''

                h2s_read() # 1  2.2.24
                ozone_read() # 2  2.2.24
                decibel_read() # 4   2.2.24
                resp_read() # 8  2.3.1
                so2_read() # 16   2.3.3
                co_read() # 64
                no2_read() # 128
                # 2.3.8  mos6814_read() # 2.3.2


                # b2 = PID U1     IC3 $49  AIN_2P-3N
                if deviceSettings['pid_enabled']:
                    if (deviceSettings['weather_shield_option'] & 4) != 0:
                        S1 = wsoIC3.get_voltage(wsoIC3.diff(chanPos=2, chanNeg=3, gain=PGA_1_024V))
                        S1 = S1 - internalSettings['pid_offset']
                        if S1 < 0: S1 = 0
                        pid_tvoc = S1 * internalSettings['pid_gain']
                        pushSensorValue('pid_tvoc', pid_tvoc)
                # EOF 2.2.23b
            # EOF 2.3.8   wsOptionAvailable
            mos6814_read() # 32     2.3.8

            # 2.4.14
            if mpsAvailable:
                value, key = mps.read()
                if key:
                    pushSensorValue(['ir_c1','temperature','pressure','humidity'][key-1], value)


            if bme280Available:
                temperature, pressure, humidity = bme280.read()
                # 2.2.23 if temperature == 22.5 and pressure == 64.7 and humidity == 83.7: # 2.2.21
                if bme280.tempDAC == 524288 and bme280.presDAC == 524288 and bme280.humDAC == 32768: # 2.2.23
                # TODO detect error using DAC values .. instead of converted values  # 2.2.21
                # if bme280.tempDAC == 22.5 and bme280.presDAC == 64.7 and bme280.humDAC == 83.7:
                    # It is most likely that the sensor have been reset, it must be initialized again.
                    temperature = pressure = humidity = 0  # 2.2.21
                    file_errLog(0, "BME280 may have been reset ... re_begin()", sdAvailable)
                    bme280Available = bme280.begin()  # Adr118
                else:
                    pushSensorValue('temperature', temperature)
                    pushSensorValue('pressure', pressure)
                    pushSensorValue('humidity', humidity)

            if sgp30Available:
                ''' cf3/7
                 #2.3.18  for test only disable humidity compensation ..
                time_sgp30 -= 1 # 2.2.1b
                if time_sgp30 <= 0: # once every 10 min save tvoc baselines + compensate humidity
                    time_sgp30 = 60 * 60 # 10 - 2.3.8
                    co2eBaseline, tvocBaseline = sgp30.get_baseline()
                    internalSettings['co2_base'] = co2eBaseline # 2.3.10  internalSetting       configurator.writeVRAM('co2_base', co2eBaseline)  2.3.8
                    internalSettings['tvoc_base'] = tvocBaseline # 2.3.10 internalSetting    configurator.writeVRAM('tvoc_base', tvocBaseline) 2.3.8
                    if tvocBaseline >= 42767 or tvocBaseline <= 22767: # 2.3.8
                        sgp30.start_measurement()
                        file_errLog(0, "VOC reinitialized", sdAvailable)

                    if bme280Available: # calibrate tvoc with humidity
                        if temperature != 0 and pressure != 0 and humidity != 0: # 2.2.21
                            dv = sgp30.calculate_dv(temperature, humidity)
                            sgp30.set_humidity(dv)
                '''


                # cf3/7
                time_sgp30 -= 1 # 2.2.1b
                if time_sgp30 <= 0: # once every 10 min save tvoc baselines + compensate humidity
                    time_sgp30 = 60 * 10
                    co2eBaseline, tvocBaseline = sgp30.get_baseline()
                    configurator.writeVRAM('co2_base', co2eBaseline)
                    configurator.writeVRAM('tvoc_base', tvocBaseline)


                    # 2.4.14
                    # if bme280Available: # calibrate tvoc with humidity
                    #    time.sleep_ms(100);
                    #    temperature, pressure, humidity = bme280.read() # !!! Temp in C !!!
                    #    time.sleep_ms(100);
                    #    if ( abs(sgp30TempThreshold - temperature) > 1) or ( abs(sgp30HumidityThreshold - humidity) > 1):
                    #        sgp30TempThreshold = temperature
                    #        sgp30HumidityThreshold = humidity
                    #        dv = sgp30.calculate_dv(temperature, humidity)
                    #        sgp30.set_humidity(dv)
                    #
                    # 2.4.14  temperature and humidity could come from MPS or BME280, data is acquired earlier in this function
                    if bme280Available or mpsAvailable :
                        if ( abs(sgp30TempThreshold - temperature) > 1) or ( abs(sgp30HumidityThreshold - humidity) > 1):
                            sgp30TempThreshold = temperature
                            sgp30HumidityThreshold = humidity
                            dv = sgp30.calculate_dv(temperature, humidity)
                            sgp30.set_humidity(dv)

                    co2e, tvoc = sgp30.read()   # print("VOC: " + str(tvoc) + " CO2:" + str(co2e))
                    pushSensorValue('co2e',co2e)
                    pushSensorValue('tvoc',tvoc)


            if ads1115Available:
                try:  # 2.2.13b
                    battI = 0
                    battV = 0
                    if deviceSettings['pycom_socket_version'] == 2: # 2.2.0b   or deviceSettings['pycom_socket_version']  == 3: # 8b
                        # battI = ads1115.get_voltage(ads1115.read(channel=0, gain=PGA_0_256V)) / 0.1  # 2.09  current batt
                        battI = ads1115.get_voltage(ads1115.read(channel=0, gain=PGA_0_256V)) / 0.1 # 2.2.7b    0.01 # 2.2.5b    0.01 #
                        battV = ads1115.get_voltage(ads1115.read(channel=1, gain=PGA_2_048V))
                    # 2.2.5b
                    if deviceSettings['pycom_socket_version'] == 20:
                        battI = ads1115.get_voltage(ads1115.read(channel=0, gain=PGA_0_256V)) / 0.01 # V2b
                        battV = ads1115.get_voltage(ads1115.read(channel=1, gain=PGA_2_048V))
                    # 2.2.0b  else: # 2.09
                    if deviceSettings['pycom_socket_version'] == 1: # 2.2.0b
                        battI = ads1115.get_voltage(ads1115.read(channel=3, gain=PGA_0_256V)) / 0.1 # current batt
                        battV = ads1115.get_voltage(ads1115.read(channel=2, gain=PGA_2_048V))
                    battV = battV * 220 / 100

                except Exception as e: # 2.2.13b
                    file_errLog(0, "Err VBatt ADS1115 - " + str(e), sdAvailable)
                    ads1115Available = False  # prevent reporting the error over and over
                    battI = 0
                    battV = 5

                pushSensorValue('instant_charge',battI)
                pushSensorValue('battery_voltage', battV)
                battC = battI * 1  # instant charge, must be computed once per second, Pos value is charge, Neg is drain
                battCsum = internalSettings['battery_charge'] # 5b   battCsum = configurator.readVRAM('battery_charge')
                battCsum = battCsum + battC  # compute battery charge
                if battCsum > (deviceSettings['battery_size'] * 3600): battCsum = (deviceSettings['battery_size'] * 3600)  #  13.2 Amp   https://www.calculator.org/properties/electric_charge.html
                if battCsum < 0: battCsum = 0
                if ( (battV >= 4.15) and (battI <= 0.01)): battCsum = deviceSettings['battery_size'] * 3600 # 2.3.6  47520  # 2.03  batt is full, initialize batt gauge
                battGauge = battCsum * 100 / (deviceSettings['battery_size'] * 3600)
                pushSensorValue('battery_charge', battCsum)
                internalSettings['battery_charge'] = battCsum   # 5b  configurator.writeVRAM('battery_charge', battCsum)
                pushSensorValue('battery_gauge', min(battGauge,100))

                if deviceSettings['pid_enabled']:
                    if (deviceSettings['weather_shield_option'] & 4) == 0: # 2.2.0b
                        # The PID sensor should be calibrated to get correct reading
                        # PIDOffset and PIDGain would be set by the calibration procedure
                        S1 = 0
                        if deviceSettings['pycom_socket_version'] == 2: # 2.2.0b   or deviceSettings['pycom_socket_version']  == 3: # 8b
                            S1 = ads1115.get_voltage(ads1115.diff(chanPos=2, chanNeg=3, gain=PGA_1_024V))
                        # 2.2.5b
                        if deviceSettings['pycom_socket_version'] == 20:
                            S1 = ads1115.get_voltage(ads1115.diff(chanPos=2, chanNeg=3, gain=PGA_1_024V))
                        # 2.2.0b else:
                        if deviceSettings['pycom_socket_version'] == 1: # 2.2.0b
                            S1 = ads1115.get_voltage(ads1115.diff(chanPos=0, chanNeg=1, gain=PGA_1_024V))

                        S1 = S1 - internalSettings['pid_offset'] # 5b   S1 = S1 - configurator.readVRAM('pid_offset')
                        if S1 < 0: S1 = 0
                        #configurator.writeVRAM('pid_offset', S1) # used for calibration
                        pid_tvoc = S1 * internalSettings['pid_gain'] # 5b   pid_tvoc = S1 * configurator.readVRAM('pid_gain')  # ppb     S1 * 1000 / 0.025   # 25mv per 1000ppb
                        pushSensorValue('pid_tvoc', pid_tvoc)

            # 2.2.1b
            if fuelgaugeAvailable == True:
                time_fuel -= 1
                if time_fuel <= 0:
                    time_fuel = 30  # one measure every 30 sec
                    battCRate = fuelGauge.read_CRate()  #  % per hour
                    battV = fuelGauge.read_vcell()
                    pushSensorValue('battery_crate',battCRate)
                    pushSensorValue('battery_voltage', battV)
                    battGauge = fuelGauge.read_SOC()
                    # 2.3.6 battCsum = (battGauge * 47520) / 100  # 100 = 47520     SOC = n    n =  SOC * 47520 / 100
                    battCsum = (battGauge * deviceSettings['battery_size'] * 3600) / 100 # 2.3.6
                    pushSensorValue('battery_charge', battCsum)
                    internalSettings['battery_charge'] = battCsum
                    pushSensorValue('battery_gauge', min(battGauge,100))

            # 2.2.1b must be the last sensor acquired because 3.3V is disturbed
            failedSensors = 0 # cf2/23
            if hm3300Available:
                try: # cf3/25
                    # CF cycle power to dust sensor, one measure per 45 sec
                    # In sleep mode, the fan stops working. It takes at least 30 seconds for stabilize when
                    # restart the fan. To obtain accurate measurement data, it is recommended that the
                    # sensor work time should not be less than 30 seconds after wake-up.
                    time_dust -= 1
                    if time_dust <= 0: # cf3/15   or battV > 4.1 : # 2.3.10  if time_dust <= 0:  # cycle the power and reading of PM sensor
                        time_dust = deviceSettings['pm_interval']  # one measure per 45 sec
                        if hm3300.PMode == 0:
                            ''' cf3/23
                            # cf3/15 reading IR takes place when PM is OFF
                            # 2.2.6b  sync IR reading with PM reading
                            if not(deviceSettings['pycom_socket_version'] == 3) and  not(deviceSettings['pycom_socket_version'] == 21): # 2.2.12b
                                if wsOptionAvailable: # 2.2.20   if wsoAvailable:
                                    # b1 = ir_c1 U2  IC3 $49  AIN_0  # U5  IC1 $4B  AIN_2P-3N
                                    if (deviceSettings['weather_shield_option'] & 2) != 0: # IR CH4 = C1 = methane ?
                                        # S1 = wsoIC1.get_voltage(wsoIC1.diff(chanPos=2, chanNeg=3, gain=PGA_4_096V))  # 2.4V max
                                        S1 = wsoIC3.get_voltage(wsoIC3.read(channel=0, gain=PGA_2_048V))
                                        # 2.2.12b  S1 = S1 - internalSettings['c1_offset']
                                        #          if S1 < 0: S1 = 0
                                        #          ir_c1 = S1 * internalSettings['c1_gain']
                                        ir_c1 = computeC1(S1) # 2.2.12b
                                        # print("3V IR_C1 %d" % (ir_c1))
                                        pushSensorValue('ir_c1', ir_c1)

                                    # b0 = IR_CO2 U5  IC1 $4B  AIN_2P-3N   # U2  IC3 $49  AIN_0        # Vb  IR_CO2 U8  IC1 $4B  AIN_0P-1N
                                    if (deviceSettings['weather_shield_option'] & 1) != 0: # IR CO2 ?
                                        # vb S1 = wsoIC1.get_voltage(wsoIC1.diff(chanPos=0, chanNeg=1, gain=PGA_1_024V))
                                        # S1 = wsoIC3.get_voltage(wsoIC3.read(channel=0, gain=PGA_4_096V)) # Va  2.4V max
                                        S1 = wsoIC1.get_voltage(wsoIC1.diff(chanPos=2, chanNeg=3, gain=PGA_2_048V))
                                        # 2.2.12b  S1 = S1 - internalSettings['co2_offset']
                                        #          if S1 < 0: S1 = 0
                                        #          ir_co2 = S1 * internalSettings['co2_gain']
                                        ir_co2 = computeCO2(S1) # 2.2.12b
                                        # print("3V IR_CO2 %d" % (ir_co2))
                                        pushSensorValue('ir_co2', ir_co2)
                            '''

                            # cf3/15 allow turning ON dust sensor only if batt is good
                            if battV > deviceSettings['caution_battery_voltage_threshold']: # cf3/15
                                powerPM(1) # cf3/15  hm3300.powerMode(1)  # wait 45 sec prior reading the sensor
                                if deviceSettings['pycom_socket_version'] == 3: # 2.2.0b
                                    # 2.2.21  DIO.writePort(0x03) # Set P0(Power shield)=1 &  P1(PM)=1
                                    DIO.wakeupPM() # 2.2.21
                                print("PM start")

                        else:
                            pm10, pm25, pm100 = hm3300.read()
                            # cf3/15   if battV < 4.1: print("PM1 %d - PM2 %d - PM10 %d - PM sleep" % (pm10, pm25, pm100))  # 2.3.10
                            print("PM1 %d - PM2 %d - PM10 %d - PM sleep" % (pm10, pm25, pm100)) # cf3/15
                            pushSensorValue('pm1.0', pm10)
                            pushSensorValue('pm2.5', pm25)
                            pushSensorValue('pm10.0', pm100)

                            # cf3/15 turn OFF dust sensor after reading no matter what
                            # cf3.15 if battV < 4.1: hm3300.powerMode(0) # 2.3.10  hm3300.powerMode(0)
                            powerPM(0) # cf3/15
                            if deviceSettings['pycom_socket_version'] == 3: # 2.2.0b
                                # 2.2.21  DIO.writePort(0x01) # Set P0(Power shield)=1 &  P1(PM)=0
                                DIO.sleepPM() # 2.2.21

                except Exception as e:  # cf3/23
                    failedSensors = 10

            # cf3/23 failedSensors = 0
        except Exception as e: # 2.2.9b
            # with open("/sd/errLog.txt", "a") as f:
            sys.print_exception(e)
            # print ("tesTraptErr: " + str(e))
            failedSensors = failedSensors + 1;
            file_errLog(0, "Err sampleSensors - " + str(e), sdAvailable)
            if deviceSettings['debugging_messages'] == False and failedSensors > 10:
                customDeepSleep(2*60*1000, True) # cf3/12   Force a Cold Reboot in 2 min
            pass

        if samplingSequence == 255:
            pass
        else:
            samplingSequence = samplingSequence * 2 # cf3/9
            if samplingSequence > 128:
                samplingSequence = 1

        # 2.2.1b
        chrono.stop()
        return chrono.read_ms()  # print("Execution time (ms):" + str(chrono.read_ms()) )

else:
    def sampleSensors(msg="Sampling...\n"):  # 2.2.2b   skyhook version of sampleSensors()
        global deviceSettings, sensorsPacket, nvsKeys
        global battV
        global sdAvailable # 2.2.10b

        chrono.reset()
        chrono.start()
        try: # 2.2.9b
            if deviceSettings['light_blinks'] == True:
                print(msg, endln='', debugging=True)

            if 'timestamp' not in sensorsPacket:
                sensorsPacket['timestamp'] = time.time()

            if BarSensOnline:
                temperature, pressure = BarSens.read()
                temperature = BarSens.Convert_C2F(temperature)
                # pressure = BarSens.Convert_Pa2ATM(pressure)
                # pushSensorValue('temp_ambient', temperature)  # F
                # pushSensorValue('pressure_atm', pressure) # Pa
                pushSensorValue('temperature', temperature) # F
                pushSensorValue('pressure', pressure) # Pa

            if DustSensOnline:
                pm10, pm25, pm100 = DustSens.read()  # return  PM1.0, 2.5, 10.0
                pushSensorValue('pm1.0', pm10)
                pushSensorValue('pm2.5', pm25)
                pushSensorValue('pm10.0', pm100)

            if AccSensOnline:
                # x, y, z = Acc.read()
                # rad = -math.atan2(y, (math.sqrt(x*x + z*z)))
                # pitch = (180 / math.pi) * rad
                pitch = Acc.do_Pitch()
                pushSensorValue('accel_x', Acc.x)  # ... add to sensorRegistery
                pushSensorValue('accel_y', Acc.y)
                pushSensorValue('accel_z', Acc.z)

                tilt = False  # tilt detection
                if abs(zeroAccX - Acc.x) > thrAccX: tilt = True
                if abs(zeroAccY - Acc.y) > thrAccY: tilt = True
                if abs(zeroAccZ - Acc.z) > thrAccZ: tilt = True
                pushSensorValue('accel_tilt', tilt)

            if IVSensOnline:   # compute power (i*u) instead of reading pw from circuit, report ish instead of ipw
                ish, ubus = IVCh1.read()  # ishunt,  ubus
                ipw, pw = IVCh1.readLSB()  # ipower, power
                # IVJSON = '"IV1":[' + '{:.2f}'.format(ish) + ', ' + '{:.0f}'.format(ubus) + ', ' + \
                #  '{:.2f}'.format(ish) +  ', ' + '{:.2f}'.format(ish*ubus) + ']'
                pushSensorValue('IVCh1_ish', ish)  # ... add to sensorRegistery
                pushSensorValue('IVCh1_ubus', ubus)
                # pushSensorValue('IVCh1_ipw', ish)
                pushSensorValue('IVCh1_pw', ish*ubus)
                ish, ubus = IVCh2.read()  # ishunt,  ubus
                pushSensorValue('IVCh2_ish', ish)
                pushSensorValue('IVCh2_ubus', ubus)
                # pushSensorValue('IVCh2_ipw', ish)
                pushSensorValue('IVCh2_pw', ish*ubus)
                ish, ubus = IVCh3.read()  # ishunt,  ubus
                pushSensorValue('IVCh3_ish', ish)
                pushSensorValue('IVCh3_ubus', ubus)
                # pushSensorValue('IVCh3_ipw', ish)
                pushSensorValue('IVCh3_pw', ish*ubus)
                ish, ubus = IVCh4.read()  # ishunt,  ubus
                pushSensorValue('IVCh4_ish', ish)
                pushSensorValue('IVCh4_ubus', ubus)
                # pushSensorValue('IVCh4_ipw', ish)
                pushSensorValue('IVCh4_pw', ish*ubus)
                ish, ubus = IVCh5.read()  # ishunt,  ubus
                pushSensorValue('IVCh5_ish', ish)
                pushSensorValue('IVCh5_ubus', ubus)
                # pushSensorValue('IVCh5_ipw', ish)
                pushSensorValue('IVCh5_pw', ish*ubus)
                ish, ubus = IVCh6.read()  # ishunt,  ubus
                pushSensorValue('IVCh6_ish', ish)
                pushSensorValue('IVCh6_ubus', ubus)
                # pushSensorValue('IVCh6_ipw', ish)
                pushSensorValue('IVCh6_pw', ish*ubus)
                ish, ubus = IVCh7.read()  # ishunt,  ubus
                pushSensorValue('IVCh7_ish', ish)
                pushSensorValue('IVCh7_ubus', ubus)
                # pushSensorValue('IVCh7_ipw', ish)
                pushSensorValue('IVCh7_pw', ish*ubus)
                ish, ubus = IVCh8.read()  # ishunt,  ubus
                pushSensorValue('IVCh8_ish', ish)
                pushSensorValue('IVCh8_ubus', ubus)
                # pushSensorValue('IVCh8_ipw', ish)
                pushSensorValue('IVCh8_pw', ish*ubus)

            if TempBattOnLine:  # if TempSensOnLine:
                # temperature = TempBatt.readC()
                temperature = TempBatt.Convert_C2F(TempBatt.readC())  # F
                pushSensorValue('temp_batt', temperature)

            if TempFitletOnLine:
                # temperature = TempFitlet.readC()
                temperature = TempFitlet.Convert_C2F(TempFitlet.readC())  # F
                pushSensorValue('temp_fitlet', temperature)

            pushSensorValue('cpu_temp', machine.temperature()) # F     (machine.temperature() - 32) / 1.8) # processor temperature in °C

            battV = deviceSettings['low_battery_voltage_threshold'] # if battery reading unavailable assume good


        except Exception as e: # 2.2.9b
            file_errLog(0, "Err sampleSensors - " + str(e), sdAvailable)
            pass

        chrono.stop()
        return chrono.read_ms()




def checkNetworks():
    global deviceSettings
    global currentPacketLoss
    connectedLTE = False
    connectedWiFi = False
    if deviceSettings['wifi_enabled']:
        password = None
        if 'wifi_pass' in deviceSettings:
            password = deviceSettings['wifi_pass']
        connectedWiFi = connectWiFi(deviceSettings['wifi_ssid'], password, deviceSettings['wifi_antenna'], fnWDog, sdAvailable) # 2.2.20
#    else:
        #disconnectWiFi()
        #connectedWiFi = False
    if deviceSettings['lte_enabled']:
        # connectedLTE = connectLTE(deviceSettings) # 2.2.4 added settings
        connectedLTE = connectLTE(deviceSettings, fnWDog, sdAvailable)  # IamAlive, sdAvailable) # 2.2.20
        if connectedLTE == False and deviceSettings['lte_carrier'] != 'standard': # 5b  LTE not found, disable it, until next reset
            deviceSettings['lte_enabled'] = False  # 5b
    #elif connectedLTE:
    #    disconnectLTE(deviceSettings)
    #    connectedLTE = False
    if connectedWiFi or connectedLTE:
        try:
            reply = ping("8.8.8.8", quiet=True)
            currentPacketLoss = (100 - ((reply[1] / reply[0]) * 100))
        except Exception as e:
            file_errLog(0,"Tried to send ping and threw error:" + str(e))

        return True
    else:
        return False

def writeToFile(file, data):
    try:
        os.remove(file)
    except:
        pass
    print('Writting to ' + file + ' ...')
    f = open(file, 'w')
    f.write(data)
    f.close()
    print('completed.')
        #return True
#    except Exception as e:
#        print("failed to write.")
#        print(e)
#        return False

def registerThing(certId):
    global mqttClient, deviceSettings, needRegister, mqttConnected
    # mqttClient.subscribe(topic="$aws/provisioning-templates/AirMonitor/provision/json/accepted")
    # mqttClient.subscribe(topic="$aws/provisioning-templates/AirMonitor/provision/json/rejected")
    thing = '{ "certificateOwnershipToken": "' + certId + '",  "parameters": { "SerialNumber": "'+ deviceSettings["device_id"] + '"} }'
    #print(thing)
    mqttClient.publish(topic="$aws/provisioning-templates/AirMonitor/provision/json", msg=thing)


def removeCerts():
 needRegister = True
 os.remove('/flash/cert/AM-' + deviceSettings["device_id"] + '.cert.pem')
 os.remove('/flash/cert/AM-' + deviceSettings["device_id"] + '.private.key')
 print('Certs removed')

def setCertificate(payload):
    global deviceSettings
    print('Recived AWS certificates... (' + payload['certificateId'] + ')')
    gc.collect()
    writeToFile('/flash/cert/AM-' + deviceSettings["device_id"] + '.cert.pem', payload['certificatePem'])
    writeToFile('/flash/cert/AM-' + deviceSettings["device_id"] + '.private.key', payload['privateKey'])
    temp = payload['certificateOwnershipToken']
    print(temp)
    registerThing(temp)

# 2.2.2b
# https://us-west-2.console.aws.amazon.com/iot/home?region=us-west-2#/test
# https://www.hivemq.com/blog/mqtt-essentials-part-5-mqtt-topics-best-practices/
#
# {"msg":"RGB", "RGB1":[1,3,2], "RGB2":[2,3,2], "RGB3":[3,3,2], "RGB4":[4,3,2]}
# {"msg":"Relay", "K1":1, "K2":0}
# {"msg":"InitCatcher"}
# {"msg":"CmdCatcher", "PUMP":[3,1]} # Pump3 ON
# topic name:  AM/AM-0002/command
# topic name:  SH/SH-0002/command     database must be created first
rxjson = "empty string"



def interpret_rxjson(): # 2.2.2b
    global rxjson
    global kSCheduleList, sendConsole
    sendConsole = 10
    try:
        if rxjson["msg"] == "uploadErrorLog": # 2.2.12b
            sendErrorLog()
            return
        if rxjson["msg"] == "clearErrorLog": # 2.2.12b
            file_errLog(2, "", sdAvailable)
            return
        if rxjson["msg"] == "listen":
            if "length" in rxjson:
                sendConsole = rxjson["length"]
        if rxjson["msg"] == "errorlog":
            f = open('/sd/errLog.txt', 'r')
            if (file_errLog(3) > 500):
                f.seek(file_errLog(3)-500)
            print(f.read())
            f.close()
        if rxjson["msg"] == "update": # 2.2.17
            if ('update_server' in deviceSettings and 'update_server_port' in deviceSettings):
                ota_instace = ota.BaseOTA(deviceSettings['update_server'], deviceSettings['update_server_port'], deviceSettings['update_server_endpoint'], fnWDog, wdt.feed, version)
                ota_instace.update(fnWDog)
            else:
                print("update_server or port are not defined.")
            return
        if rxjson["msg"] == "config":
            deviceSettings.update(rxjson["config"])
            configurator.writeFlashConfig(deviceSettings)
            mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/config', msg=buildMQTTConfigPacket(), qos=MQTTqos)
        if rxjson["msg"] == "InitCatcher": # 2.2.17
            catcherInit()
            return
        if rxjson["msg"] == "eval":
            try:
                print(eval(rxjson["eval"]))
            except Exception as e:
                sys.print_exception(e)
        if rxjson["msg"] == "CmdCatcher": # 2.2.17
            Pump, Cmd = rxjson.get('PUMP', [0,0])
            catcherCmd(Pump, Cmd)
            return

        if rxjson["msg"] == "CalCO2": # 2.2.12b
            calibCO2()
            writeFram()
            return

        if rxjson["msg"] == "CalCH4": # 2.2.12b
            calibC1()
            writeFram()
            return

        if rxjson["msg"] == "CalSO2": # 2.3.3
            calibSO2()
            writeFram()
            return
        if rxjson["msg"] == "CalNO2": # 2.3.3
            calibNO2()
            writeFram()
            return

        if rxjson["msg"] == "CalCO": # 2.3.3
            calibCO()
            writeFram()
            return

        if rxjson["msg"] == "CalH2S": # 2.3.3
            calibH2S()
            writeFram()
            return

        if rxjson["msg"] == "CalO3": # 2.3.3
            calibO3()
            writeFram()
            return

        if rxjson["msg"] == "CalRESP": # 2.3.3
            calibRESP()
            writeFram()
            return

        if rxjson["msg"] == "CalNH3": # 2.3.3
            calibMOS6814()
            writeFram()
            return

        if rxjson["msg"] == "CLRsmp": # 2.2.10b  Clear SmpLog file
            file_smpLog(2, "", sdAvailable)
            readIdxfile_smpLog = file_smpLog(3, "", sdAvailable)
            return

        if rxjson["msg"] == "RST":
            print("Reset processor in 3sec")
            time.sleep(3)
            fnWDog(3) # 2.2.20  machine.reset()

        if rxjson["msg"] == "Ping":
            if Skyhook:
                packetToSend = buildMQTTSkyHookPacket()
                mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/status', msg=packetToSend, qos=MQTTqos)
                print("Sending Skyhook status over MQTT.")
                print(packetToSend) # , debugging=True)
            else:
                mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/command', msg="Pong", qos=MQTTqos)
                packetToSend = buildMQTTStatusPacket()
                mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/status', msg=packetToSend, qos=MQTTqos)
                return

        if Skyhook == False:
            print("AM Conclude Interpret")
            return


        if rxjson["msg"] == "RGB" :
            red,green,blue = rxjson.get('RGB1', [0,0,0])
            print("Interpret RGB - r1:" + str(red) + " g1:" + str(green) + " b1:" + str(blue))
            rgbStrip1.setColor(red,green,blue)

            red,green,blue = rxjson.get('RGB2', [0,0,0])
            rgbStrip2.setColor(red,green,blue)

            red,green,blue = rxjson.get('RGB3', [0,0,0])
            rgbStrip3.setColor(red,green,blue)

            red,green,blue = rxjson.get('RGB4', [0,0,0])
            rgbStrip4.setColor(red,green,blue)

            file_rgbConfig(0)  # save rgb setting
            # send_RGB()  # inform web app of changes

        if rxjson["msg"] == "Relay" :
            kParse = [255] * 8
            kParse[0] = rxjson.get('K1', 255)  #    [255]
            kParse[1] = rxjson.get('K2', 255)
            kParse[2] = rxjson.get('K3', 255)
            kParse[3] = rxjson.get('K4', 255)
            kParse[4] = rxjson.get('K5', 255)
            kParse[5] = rxjson.get('K6', 255)
            kParse[6] = rxjson.get('K7', 255)
            kParse[7] = rxjson.get('K8', 255)
            for i in range(8):
                if (kParse[i] != 255):  #    [255]):
                    kSCheduleList[i][0] = kParse[i]

            file_KConfig(0)
            skyhook_Relays() #  manage relays schedule
            # send_Relay()   # inform web app of changes

    except Exception as e: # 2.2.9b
        file_errLog(0, "Err interpret_rxjson - " + str(e), sdAvailable) # 2.2.9b   print("Err: interpret_rxjson")
        return

    print("Conclude Interpret")



def printSubmsg(topic, msg): # 2.2.2b
    global rxjson, needRegister, mqttConnected

    try:
        print ('MQTT message received')
        #print ('cb_topic: ' + str(topic))
        #print ('cb_msg: ' + str(msg))
        # 2.2.2b  print(topic, msg)

        rxmessage = msg.decode('utf8').replace("'", '"')
        # print(rxmessage)
        # print('- ' * 20)

        rxjson = json.loads(rxmessage)
        # s = json.dumps(rxjson)
        # print(s)
        # print('- ' * 20)

    except Exception as e: # 2.2.9b
        file_errLog(0, "Err printSubmsg - " + str(e), sdAvailable) # 2.2.9b    print("Err: printSubmsg")
        rxjson = '' # 2.2.17
        return

    ''' 2.2.17
    print(topic)
    msg = json.loads(msg.decode('utf8').replace("'", '"'))
    print(msg)
    if topic == b'$aws/certificates/create/json/accepted': #mf
        setCertificate(msg)
    elif topic == b'$aws/provisioning-templates/AirMonitor/provision/json/accepted':
        machine.reset()
    else:
        interpret_rxjson()
    '''

    # 2.2.17
    print(topic)
    #print(rxjson)
    if topic == b'$aws/certificates/create/json/accepted':
        setCertificate(rxjson)
        rxjson = ''
    elif topic == b'$aws/provisioning-templates/AirMonitor/provision/json/accepted':
        needRegister = False
        mqttConnected = False
        mqttClient.disconnect()
         # 2.2.20  machine.reset()
    else:
        # 2.2.17  no changed required messages are checked in sync with main SampleLoop
        # mqttClient.check_msg() (3)   # acquire and process messages

        # pass  # moved to main SampleLoop    -   interpret_rxjson()
        interpret_rxjson() # Ok  because in syn with main task
        rxjson = ''


failedMQTT = 0
failedSensors = 0
def checkMQTT():
    global deviceSettings, mqttClient, mqttConnected, needRegister, internetConnected, failedMQTT

    if not mqttConnected or needRegister:
        attempts = 3
        mqttConnected = False
        while not mqttConnected and attempts > 0:
            attempts = attempts - 1
            try:
                if (needRegister):
                    mqttClient = MQTTClient("AirMonitor", "a1njj292w2vjt1-ats.iot.us-west-2.amazonaws.com", port=8883, ssl=True, ssl_params={"certfile": "/flash/cert/global.cert.pem","keyfile": "/flash/cert/global.private.key","ca_certs": "/flash/cert/root-CA.crt"})
                    print('MQTT connecting as global.')
                else:
                    mqttClient = MQTTClient("AM-"+deviceSettings["device_id"], "a1njj292w2vjt1-ats.iot.us-west-2.amazonaws.com", port=8883, ssl=True, ssl_params={"certfile": "/flash/cert/AM-" + deviceSettings["device_id"] + ".cert.pem","keyfile": "/flash/cert/AM-" + deviceSettings["device_id"] + ".private.key","ca_certs": "/flash/cert/root-CA.crt"})
                    print('MQTT connecting as ' + "AM-"+deviceSettings["device_id"] + ".")
                mqttClient.set_callback(printSubmsg)
                wdt.init(999999)
                file_errLog(0, "Connecting to MQTT...", sdAvailable)
                mqttClient.connect()
                wdt.init(120000)
                # https://us-west-2.console.aws.amazon.com/iot/home?region=us-west-2#/test
                # https://www.hivemq.com/blog/mqtt-essentials-part-5-mqtt-topics-best-practices/
                # topic name:  AM/AM-0002/command      Message payload: { "message": "Hello from AWS IoT console" }
                # topic name:  SH/SH-0002/command
                # if Skyhook: # 2.2.2b
                #    mqttClient.subscribe(topic="SH/SH-" + deviceSettings['device_id'] + "/command")
                # else:
                #    mqttClient.subscribe(topic="AM/AM-" + deviceSettings['device_id'] + "/command")
                mqttClient.subscribe(topic="AM/AM-" + deviceSettings['device_id'] + "/command")
                print("Subscribed to commands")
                file_errLog(0, "Successfull connected to MQTT.", sdAvailable)
                # ... loop mqttClient.check_msg() to check if message are available


                print("Connected to mqtt.")
                mqttConnected = True
                if (needRegister): # once connected request a certificate
                    mqttClient.subscribe(topic="$aws/certificates/create/json/accepted")
                    print("Sending certificate request...")
                    mqttClient.publish(topic="$aws/certificates/create/json", msg="")
                failedMQTT = 0;
            except Exception as e: # 2.2.9b   OSError as e:
                # 2.2.9b  print("Failed to connect to mqtt.")
                # 2.2.9b  print(str(e))
                file_errLog(0, "Err checkMQTT - " + str(e), sdAvailable) # 2.2.9b
                file_errLog(0, "Packet Loss: " + str(currentPacketLoss) + "%", sdAvailable)
                file_errLog(0, "Ping Times: " + str(currentPingTime) + "ms", sdAvailable)
                mqttConnected = False
                gc.collect()
                internetConnected = False
                failedMQTT = failedMQTT + 1
                try:
                    mqttClient.disconnect()
                except Exception as e:
                    print(e)
                if failedMQTT > 10:
                    file_errLog(0, "MQTT failed to many times rebooting...", sdAvailable)
                    removeCerts() #refresh certs
                    customReset()

        return mqttConnected

    else:
        return True

ota_instace = None

# Global LoRa dictionary - this dictionary should be mirrored on the lambda function and if new elements are added they will be intepreted on the server
# types/topics
l_topics = { 'confirm': '-', 'ping': '0', 'pong': '1', 'sample': '2', 'status': '3', 'proxy': '4', 'struct': '5', 'structMsg': '6' }
key_words = { b'1': 'pm1.0', b'2':'pm2.5', b'9':'pm10.0', b'u':'up_time', b'e':'version', b'l':'imei' , b'p': 'pressure', b'i': 'cpu_temp', b'v': 'tvoc', b't': 'temperature', b'c': 'co2e', b'h': 'humidity', b'g': 'battery_gauge', b'I': 'instant_charge', b'V': 'battery_voltage' }
topic_words = { b's': 'sensors', b'S': 'status', b'n': 'network', b'm':'misc', b'c':'commands', b'C': 'config'}
# units are assumed - convert on server side
#unit_words = { 'b': 'ppb', 'm': 'ppm', 'c': 'c', 'C': 'C', 'F':'F', '-':'units', 'a':'ATM', 'R':'%RH', 'M':'M/s', 'f': 'ft/s', 'i':'mph', 'd': 'deg', 's':'s', 'V':'V'  }
k_broad = b'0' # broadcast message - anyone can reply -mostly just for ping

# initialize the LoRa radio in LORA mode
if deviceSettings:
    if deviceSettings['lora_enabled']:
        lora = LoRa(mode=LoRa.LORA, region=LoRa.US915)

            # create a raw LoRa socket
        s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)
            # set the LoRaWAN data rate
        s.setsockopt(socket.SOL_LORA, socket.SO_DR, 3)
        lora.coding_rate(LoRa.CODING_4_7)
        lora.bandwidth(LoRa.BW_125KHZ)

#up to last 4 samples
packet_buffer = []

# replied with gateway access
meshNodeDevices = {}
structs = {}
closestNode = { 'jumps': 99 }
distanceToGateway = 0
lastPingTime = 0
pingId = b'8'

def generateKey():
    global meshNodeDevices, pingId, k_broad

    key = b'3'
    goodKey = False
    while goodKey == False:
        key = os.urandom(1)
        if key != pingId and key != k_broad and (len([i for i in meshNodeDevices if meshNodeDevices[i]['key'] == key]) == 0):
            goodKey = True
    return key

pingId = generateKey()


def addNodeDevice(parsedMsg):
    global l_topics, k_broad, loRaConnected
    global meshNodeDevices, closestNode, distanceToGateway

    deviceAdded = False
    newDevice = False
    if parsedMsg['id'] in meshNodeDevices:
        print('Device already added - updating.', debugging=True)
        print(parsedMsg, debugging=True)
        deviceAdded = True
        meshNodeDevices[parsedMsg['id']]['jumps'] = parsedMsg['jumps']
        if parsedMsg['jumps'] == 0:
            if 'id' in closestNode:
                if parsedMsg['id'] == closestNode['id']:
                    closestNode = { 'jumps': 99 }
        meshNodeDevices[parsedMsg['id']]['timeOffset'] = parsedMsg['time']
        meshNodeDevices[parsedMsg['id']]['stale'] = 15
        if parsedMsg['type'] == l_topics['pong']: # probaly a device that rebooted
            if meshNodeDevices[parsedMsg['id']]['key'] != parsedMsg['data']:
                print('Key changed, resending structure.')
                meshNodeDevices[parsedMsg['id']]['key'] = parsedMsg['data']
                newDevice = True
                if closestNode['id'] == parsedMsg['id']:
                    closestNode['key'] = parsedMsg['data']
    #generate key that will be used by sender to address this device
    if parsedMsg['type'] == l_topics['ping'] and not deviceAdded:
        assignedKey = generateKey()
        time.sleep_ms(int(str(machine.rng())[0:3]))
        newDevice = True
    elif parsedMsg['type'] == l_topics['pong'] and not deviceAdded:
        assignedKey = parsedMsg['data']
        time.sleep_ms(int(str(machine.rng())[0:3]))
        newDevice = True
    if not deviceAdded:
        meshNodeDevices[parsedMsg['id']] = {"jumps": parsedMsg['jumps'], "key": assignedKey, "timeOffset": parsedMsg['time'], "stale": 10 }
    if meshNodeDevices[parsedMsg['id']]['timeOffset'] > 100000000 and time.time() < 100000000:
        rtc.init(time.gmtime(meshNodeDevices[parsedMsg['id']]['timeOffset'])[:-2])
        print("RTC set from LoRa device.", debugging=True)
    jumpsInt = parsedMsg['jumps']
    if closestNode['jumps'] > jumpsInt and jumpsInt != 0:
        loRaConnected = True
        closestNode['jumps'] = jumpsInt
        closestNode['id'] = parsedMsg['id']
        closestNode['key'] = meshNodeDevices[parsedMsg['id']]['key']
        closestNode['timeOffset'] = parsedMsg['time']
        distanceToGateway = jumpsInt + 1
    return newDevice, meshNodeDevices[parsedMsg['id']]['key']
    #print(obj)
    #meshNodeDevices.append(obj)

# accepts ping message
def sendPong(parsedMsg):
    global l_topics, k_broad, meshNodeDevices, distanceToGateway
    if distanceToGateway >= 16:
        distanceToGateway = 15
    toSend = buildMsg(parsedMsg['data'], l_topics['pong'], data=struct.pack('hb',int(deviceSettings['device_id']), distanceToGateway) + meshNodeDevices[parsedMsg['id']]['key'])
    print('Sending pong...')
    sendLoRa(toSend)

sampleStructKey = generateKey()
statusStructKey = generateKey()
while statusStructKey == sampleStructKey:
    statusStructKey = generateKey()
#b'u':'up_time', b'e':'version', b'l':'imei' , b'p': 'pressure', b'i': 'cpu_temp', b'v': 'tvoc', b't': 'temperature', b'c': 'co2e', b'h': 'humidity', b'g': 'battery_gauge', b'I': 'instant_charge', b'V': 'battery_voltage' }
sampleKeys = b'pivtchg2'
#b=1, h=2, l=4, d=6, q=8
sampleStruct = b'hbhbhbbh'
statusKeys = b'uelV'
statusStruct = b'lhqh'

def sendStructs(key):
    global l_topics, k_broad, meshNodeDevices, distanceToGateway

    toSend = buildMsg(key, l_topics['struct'], data=sampleStructKey+b's'+sampleStruct+sampleKeys, shortTime=True)
    sendLoRa(toSend)
    time.sleep_ms(100)
    toSend = buildMsg(key, l_topics['struct'], data=statusStructKey+b'S'+statusStruct+statusKeys, shortTime=True)
    sendLoRa(toSend)
    print("Sending LoRa data structure...")

def sendPing():
    global l_topics, k_broad, meshNodeDevices
    global distanceToGateway, lastPingTime, pingId

    lastPingTime = time.time()
    pingId = generateKey()
    if distanceToGateway >= 16:
        distanceToGateway = 15
    toSend = buildMsg(k_broad, l_topics['ping'], data=struct.pack('hb',int(deviceSettings['device_id']), distanceToGateway) + pingId)
    print('Sending ping...')
    sendLoRa(toSend)

def buildMsg(key, type, data=None, shortTime=False, altId=False):
    global l_topics, deviceSettings
    #key of 0000 is broadcast channel
    #key(4)|id(16)|jumps(4)|type(4)| - time(32) or time offset (16) - data
    #1|4|1|1|8|1 - 16 header 51

    if altId == False:
        msg = key + type
    else:
        msg = key + l_topics['proxy'] + struct.pack('h',int(altId)) + type

    if shortTime != False:
        device = [item for item in meshNodeDevices if meshNodeDevices[item].get('key') == key]
        if len(device) != 0:
            msg = msg + struct.pack('h',time.time() - meshNodeDevices[device[0]]['timeOffset'])
        else:
            if 'timeOffset' in closestNode:
                msg = msg +  struct.pack('h',time.time() - closestNode['timeOffset'])
            else:
                msg = msg +  struct.pack('h',time.time())
    else:
        msg = msg + struct.pack('l', time.time())
    if data != None:
        msg = msg + data
    return msg


def parseStructMsg(data):
    global structs, key_words

    data = bytearray(data)
    try:
        res = struct.unpack(structs[bytes(data[0:1])]['coding'], data[1:])
        ret = {}
        for index in range(0,len(structs[bytes(data[0:1])]['keys'])):
            key = bytes(structs[bytes(data[0:1])]['keys'][index:index+1])
            if key in key_words:
                ret[key_words[key]] = { "value": res[index] }
                if key_words[key] == 'battery_voltage':
                    ret[key_words[key]] = { "value": res[index]/10 }
                if key_words[key] == 'version':
                    ret[key_words[key]] = '.'.join(list(str(res[index])))
            else: # unknown key send as is
                ret[str(key)] = { 'value': res[index] }

    except Exception as e: # 2.2.9b
        file_errLog(0, "Err parseStructMsg - " + str(e), sdAvailable) # 2.2.9b
        ret = False

    return ret, structs[bytes(data[0:1])]['topic']


def parseStruct(data):
    global key_words, topic_words

    data = bytearray(data)
    leng = len(data)
    return bytes(data[0:1]), { 'coding': data[2:int(leng/2)+1].decode(), 'keys': data[int(leng/2)+1:], 'topic': topic_words[bytes(data[1:2])] }

def parseMsg(rx_data):
    parsedMsg = {}
    data = bytearray()
    print('Recived LoRa Raw: ' + str(rx_data), debugging=True)
    try:
        data.extend(rx_data)
        parsedMsg['key'] = bytes(data[0:1])
        parsedMsg['type'] = data[1:2].decode()
        if (parsedMsg['type'] == '1' or parsedMsg['type'] == '0'): # ping/pongs contain full timestamp
            parsedMsg['time'] = struct.unpack('l',data[2:6])[0]
            parsedMsg['id'] = "{:04d}".format(struct.unpack('h', data[6:8])[0])
            parsedMsg['jumps'] = struct.unpack('b',data[8:9])[0]
            parsedMsg['data'] = bytes(data[9:])
        elif parsedMsg['type'] == l_topics['proxy']:
            parsedMsg['type'] = data[2:3].decode()
            parsedMsg['proxy_id'] = "{:04d}".format(struct.unpack('b', data[3:5])[0])
            parsedMsg['time'] = struct.unpack('h',data[5:7])[0]
            parsedMsg['data'] = bytes(data[7:])
        elif parsedMsg['type'] == l_topics['struct']:
            parsedMsg['time'] = struct.unpack('h',data[2:4])[0]
            parsedMsg['struct_id'], parsedMsg['parsed'] = parseStruct(bytes(data[4:]))
            parsedMsg['data'] = bytes(data[4:])
        elif parsedMsg['type'] == l_topics['structMsg']:
            parsedMsg['time'] = struct.unpack('h',data[2:4])[0]
            parsedMsg['data'] = bytes(data[4:])
            parsedMsg['parsed'], parsedMsg['topic']  = parseStructMsg(bytes(data[4:]))
        else:
            parsedMsg['time'] = struct.unpack('h',data[2:4])[0]
            parsedMsg['data'] = bytes(data[4:])

        if parsedMsg['time'] < 0:
            raise RuntimeError('Bad time')
        return parsedMsg

    except Exception as e: # 2.2.9b
        file_errLog(0, "Err parseMsg - " + str(e), sdAvailable) # 2.2.9b     print('Invalid msg.', debugging=True)
        return False


def sendLoRa(data):
    global s, meshNodeDevices, closestNode

    #wrap packet
    packet = data
    s.setblocking(True)
    s.send(packet)
    s.setblocking(False)
    print('Sending via LoRa: ' + str(packet), debugging=True)
    for item in meshNodeDevices: #each send reduce staleness
        meshNodeDevices[item]['stale'] = meshNodeDevices[item]['stale'] - 1
        if meshNodeDevices[item]['stale'] < 0:
            if 'id' in closestNode:
                if item == closestNode['id']:
                    closestNode = { 'jumps': 99 }
            del meshNodeDevices[item]

def lora_cb(lora):
    global l_topics, k_broad, meshNodeDevices, structs
    global s, mqttClient, mqttConnected

    try:
        events = lora.events()
        if events & LoRa.RX_PACKET_EVENT:
            rx_data = s.recv(64)
            if rx_data != b'': #check if empty
                data = parseMsg(rx_data)
                if data != False:
                    print('Recived LoRa Parsed: {}'.format(data), debugging=True)
                    if data['key'] == k_broad and data['type'] == l_topics['ping']:
                        print('Ping recived.')
                        newDevice, newKey = addNodeDevice(data)
                        time.sleep_ms(int(str(machine.rng())[0:3])+200)
                        sendPong(data)
                        if newDevice:
                            sendStructs(newKey)

                    if data['key'] == pingId and data['type'] == l_topics['pong']:
                        newDevice, newKey = addNodeDevice(data)
                        print('Pong recived.')
                        if newDevice:
                            sendStructs(newKey)

                    knowDevice = [item for item in meshNodeDevices if meshNodeDevices[item].get('key') == data['key']]
                    if len(knowDevice) != 0:
                        knowDevice = knowDevice[0]
                        print(knowDevice + " is talking to me.", debugging=True)
                        if 'struct_id' in data:
                            structs[data['struct_id']] = data['parsed']
                            print('Saved stucture {}.'.format(str(data['struct_id'])))
                        if mqttConnected:
                            if data['type'] == l_topics['structMsg']:
                                sendPacket = data['parsed']
                                sendPacket['timestamp'] = meshNodeDevices[knowDevice]['timeOffset'] + data["time"]
                                if 'proxy_id' in data:
                                    sendPacket['device_id'] = "AM-" + str(data["proxy_id"])
                                else:
                                    sendPacket['device_id'] = "AM-" + str(knowDevice)

                                sendPacket['proxy'] = True
                                try:
                                    mqttClient.publish(topic="AM/AM-" + deviceSettings["device_id"] + "/" + str(data['topic']), msg=str(sendPacket).replace("'", '"').replace('True', 'true').replace('False','false'), qos=MQTTqos)
                                    print("Sending MQTT Proxy packet.")
                                    print("{}: {}".format(data['topic'], sendPacket), debugging=True)
                                except Exception as e: # 2.2.9b
                                    file_errLog(0, "Err lora_cb - " + str(e), sdAvailable) # 2.2.9b    print("Failed to send over MQTT. (proxy)")
                                    mqttConnected = False
                                    rgbStrip.blinkState("warning",2,250) # 2x fast orange

                        else:
                            #send it via proxy
                            if 'key' in closestNode:
                                if 'proxy_id' in data:
                                    proxyMsg = buildMsg(closestNode['key'], data['type'], data=data['data'], altId=data['proxy_id'])
                                else:
                                    proxyMsg = buildMsg(closestNode['key'], data['type'], data=data['data'], altId=knowDevice)
                                sendLoRa(proxyMsg)
                                print("Sending via LoRa proxy.")
                                print(proxyMsg, debugging=True)
                            else:
                                print("Can't send via LoRa proxy - no known gateways")

    except Exception as e: # 2.2.9b  OSError as e:
        # 2.2.9b  print("LoRa Error")
        # 2.2.9b  print(str(e))
        file_errLog(0, "LoRa Error - " + str(e), sdAvailable) # 2.2.9b
        pass
            # rx_data
            # try:
                # mqttClient.publish(topic=data['t'], msg=data['d'])
                # data['s'] = 1
            # except:
                # data['s'] = 0
            # sendLoRa(data)

def initLoRa():
    lora.callback(trigger=(LoRa.RX_PACKET_EVENT), handler=lora_cb)
    print('LoRa gateway online.')


def checkLoRa():
    # if no path to gateway aka connection gone stale, send ping else return true
    sendPing()
    time.sleep_ms(100)
    if 'id' not in closestNode:
        return False
    else:
        return True


# ============================================================================

def OnWebSocketAccepted(microWebSrv2, webSocket):
    global wss, deviceSettings, lteData
    print('Example WebSocket accepted:', debugging=True)
    print('   - User   : %s:%s' % webSocket.Request.UserAddress, debugging=True)
    print('   - Path   : %s'    % webSocket.Request.Path, debugging=True)
    print('   - Origin : %s'    % webSocket.Request.Origin, debugging=True)
    wss[webSocket.Request.UserAddress[1]] = { "subscriptions": [], "ws": webSocket }
    if webSocket.Request.Path.lower() == '/logs' :
        #WSJoinLogs(webSocket)
        pass
    else :
        webSocket.SendTextMessage('{ "config": ' + buildMQTTConfigPacket() + '}')
        if 'lte_enabled' in deviceSettings:
            if (deviceSettings['lte_enabled']):
                webSocket.SendTextMessage('{ "lte": ' + jsonToStr(json.dumps(lteData)) + '}')
        webSocket.OnTextMessage   = OnWebSocketTextMsg
        webSocket.OnBinaryMessage = OnWebSocketBinaryMsg
        webSocket.OnClosed        = OnWebSocketClosed

# ============================================================================

def OnWebSocketTextMsg(webSocket, msg):
    global wss
    try:
        jsonMSg = json.loads(msg)
        for key in jsonMSg.keys():
            if (key == 'time'):
                setTime(eval(str(jsonMSg['time'])))
            if (key == 'console'):
                eval(str(jsonMSg['console']))
            if (key == 'config'): ## updates the config and sends back updated one, if sent blank will send current config
                deviceSettings.update(jsonMSg['config'])
                configurator.writeFlashConfig(deviceSettings)
                webSocket.SendTextMessage('{ "config": %s }' % buildMQTTConfigPacket())
                if 'device_id' in jsonMSg['config']:
                    machine.reset()
            #if (key == 'subscribe'): ## adds a sensor to subscriptions - will send updates on event
            #    if (jsonMSg['subscribe'] == []): ## send empty subscribe to list current subscriptions
            #        webSocket.SendTextMessage("{ 'subscriptions': '%s' }" % str(wss[webSocket.Request.UserAddress[1]]['subscriptions']))
            #    for sensor in jsonMSg['subscribe']:
            #        wss[webSocket.Request.UserAddress[1]]['subscriptions'].append(sensor)
            #if (key == 'unsubscribe'):
            #    for sensor in jsonMSg['unsubscribe']: ## removes a sensor from subscriptions
            #        wss[webSocket.Request.UserAddress[1]]['subscriptions'].remove(sensor)

    except Exception as e: # 2.2.9b   OSError as e:
        # 2.2.9b  print("WS-ERROR> Invalid JSON Received.", debugging=True)
        # 2.2.9b print(e, debugging=True)
        file_errLog(0, "Err OnWebSocketTextMsg - " + str(e), sdAvailable) # 2.2.9b

    print('WebSocket text message: %s' % msg, debugging=True)

    #webSocket.SendTextMessage('Received "%s"' % msg)

# ------------------------------------------------------------------------

def OnWebSocketBinaryMsg(webSocket, msg) :
    print('WebSocket binary message: %s' % msg, debugging=True)

# ------------------------------------------------------------------------

def OnWebSocketClosed(webSocket) :
    global wss
    print('WebSocket %s:%s closed' % webSocket.Request.UserAddress, debugging=True)
    del wss[webSocket.Request.UserAddress[1]]

# ============================================================================

def startSettingsAP():
    global deviceSettings, apTimer, pyhtmlMod, wsMod

    apTimer.reset()
    apTimer.start()
    swNet_led(True)
    tempSettings = deviceSettings
    oldAPSetting = deviceSettings['ap_enabled']
    tempSettings['ap_enabled'] = True
    wlan, apName = initWiFi(tempSettings)
    wlan.ifconfig(id=1, config=('192.168.0.1', '255.255.255.0', '192.168.0.1', '192.168.0.1')) # 192.168.0.107 when connected directly
    wlan.ifconfig(id=0, config=('dhcp')) # ip address is auto assigned when connecting to network
    print("Setup access point live - {}.".format(apName))
    print("Direct Connect IP Address: {}".format(wlan.ifconfig(id=1)[0]))
    if deviceSettings['wifi_enabled']:
        connectWiFi(deviceSettings['wifi_ssid'], deviceSettings['wifi_pass'], deviceSettings['wifi_antenna'], fnWDog, sdAvailable) # 2.2.20

    # Loads the PyhtmlTemplate module globally and configure it,
    try:
        pyhtmlMod = MicroWebSrv2.LoadModule('PyhtmlTemplate')
        pyhtmlMod.ShowDebug = True
        pyhtmlMod.SetGlobalVar('ap_name', apName)
        for key in deviceSettings.keys():
            pyhtmlMod.SetGlobalVar(key, deviceSettings[key])
    except:
        print("PyhtmlTemplate already loaded.")
        for key in deviceSettings.keys():
            pyhtmlMod.SetGlobalVar(key, deviceSettings[key])

    # Loads the WebSockets module globally and configure it,
    try:
        wsMod = MicroWebSrv2.LoadModule('WebSockets')
        wsMod.OnWebSocketAccepted = OnWebSocketAccepted
    except:
        print("WebSockets already loaded.")
    # Instanciates the MicroWebSrv2 class
    mws = MicroWebSrv2()
    # For embedded MicroPython, use a very light configuration,
    mws.SetEmbeddedConfig()
    mws.NotFoundURL = '/'
    mws.StartManaged()
    deviceSettings['ap_enabled'] = oldAPSetting
    return mws


if 'device_id' not in deviceSettings:
    @WebRoute(GET, '/index.html', name='Setup')
    def RequestDownloadGet(microWebSrv2, request):
        request.Response.ReturnFile('/setup.html')

@WebRoute(GET, '/downloadData', name='DownloadData')
def RequestDownloadGet(microWebSrv2, request):
    request.Response.SetHeader('Content-Disposition', 'attachment; filename="AM-' + deviceSettings['device_id'] + '_sensors_' + str(time.time()) + '.json"')
    #request.Response.SetHeader('filename', 'Samples-' + str(time.time()) + ".json")
    request.Response.ReturnFile('/sd/smpLog.txt')

@WebRoute(GET, '/deleteData', name='DeleteLog')
def RequestDownloadGet(microWebSrv2, request):
    file_smpLog(2, "", sdAvailable)
    readIdxfile_smpLog = file_smpLog(3, "", sdAvailable)
    request.Response.ReturnOk()


@WebRoute(GET, '/downloadLog', name='DownloadLog')
def RequestDownloadGet(microWebSrv2, request):
    request.Response.SetHeader('Content-Disposition', 'attachment; filename="AM-' + deviceSettings['device_id'] + '_Log_' + str(time.time()) + '.txt"')
    #request.Response.SetHeader('filename', 'Log-' + str(time.time()) + ".json")
    request.Response.ReturnFile('/sd/errLog.txt')

@WebRoute(GET, '/deleteLog', name='DeleteLog')
def RequestDownloadGet(microWebSrv2, request):
    file_errLog(2, "", sdAvailable)
    request.Response.ReturnOk()

def buildMQTTSamplePacket(packetToSend, proxy=False, proxyDevice=None):
    global sensorRegistery, deviceSettings

    if Skyhook: # 2.2.2b
        packetToSend['device_id'] = "SKY-" + deviceSettings['device_id']
    else:
        packetToSend['device_id'] = "AM-" + deviceSettings['device_id']

    if proxyDevice:
        packetToSend['device_id'] = proxyDevice
    if proxy == True:
        packetToSend['proxy'] = True
    return str(packetToSend).replace("'", '"').replace('True', 'true').replace('False','false')


def buildLoRaSamplePacket(packetReadyToFormat):
    global sampleStruct, sampleStructKey

    try:
        sendPacket = struct.pack(sampleStruct, int(packetReadyToFormat['pressure']['value']), int(packetReadyToFormat['cpu_temp']['value']), int(packetReadyToFormat['tvoc']['max']), int(packetReadyToFormat['temperature']['value']), int(packetReadyToFormat['co2e']['max']), int(packetReadyToFormat['humidity']['value']), int(packetReadyToFormat['battery_gauge']['value']), int(packetReadyToFormat['pm2.5']['value']))

    except Exception as e: # 2.2.9b
        file_errLog(0, "Err buildLoRaSamplePacket - " + str(e), sdAvailable) # 2.2.9b    print('Sample packet incomplete.')
        return ''

    if 'key' in closestNode:
        msg = buildMsg(closestNode['key'], l_topics['structMsg'], data=sampleStructKey + sendPacket, shortTime=True)
    else:
        msg = ''
    return msg



def buildMQTTConfigPacket(proxyDevice=None):
    global deviceSettings, lteScan, eID

    packetToSend = deviceSettings.copy()
    if ('device_id' in deviceSettings):
        if Skyhook: # 2.2.2b
            packetToSend['device_id'] = "SKY-" + deviceSettings['device_id']
        else:
            packetToSend['device_id'] = "AM-" + deviceSettings['device_id']
    if 'fipy_id' in deviceSettings:
        packetToSend['fipy_id'] = deviceSettings['fipy_id'] # FIPY-0001
    packetToSend['fipy_mac'] = ubinascii.hexlify(machine.unique_id()).decode()
    packetToSend['timestamp'] = time.time()
    packetToSend['firmware_version'] = os.uname().release
    packetToSend['telnet'] = str(wlan.ifconfig(id=0)[0]) # 8b
    packetToSend['reset'] = resetMessage # 8b
    packetToSend['pycom_socket_version'] = deviceSettings['pycom_socket_version']
    if 'weather_shield_option' in deviceSettings:
        packetToSend['weather_shield_option'] = deviceSettings['weather_shield_option']
    if sdAvailable:
        packetToSend['sd_free_space'] = str(os.getfree("/sd"))
    # 8b
    if abs(GPSAccuracy) < 0.001:  # not(GPSLat == 0) and not(GPSLong == 0): # if l76Available == True:
        packetToSend['GPSLat'] = str(GPSLat)
        packetToSend['GPSLong'] = str(GPSLong)

    if proxyDevice:
        packetToSend['device_id'] = proxyDevice
    if 'wifi_pass' in packetToSend:
        del packetToSend['wifi_pass']
        # 2.2.4 added settings

    print('Getting LTE info')
    if eID != None:
        packetToSend['eid'] = eID
    try:
        #info = infoLTE(deviceSettings['lte_enabled'])
        # Use the existing LTE information, before the connection, instad of calling infoLTE again
        packetToSend['imei'] = lteData['details']['imei']
        packetToSend['iccid'] = lteData['details']['iccid']
    except Exception as e:
        # 2.2.20 print("Unable to get lte info.")
        # 2.2.20 print(e)
        file_errLog(0, "Unable to get lte info - " + str(e), sdAvailable) # 2.2.20
        pass
    return str(packetToSend).replace("'", '"').replace('True', 'true').replace('False','false')



# 2.2.2b
def buildMQTTSkyHookPacket():
    packetToSend = {}

    red, green, blue = rgbStrip1.np[0]
    packetToSend['RGB1r'] = red
    packetToSend['RGB1g'] = green
    packetToSend['RGB1b'] = blue

    red, green, blue = rgbStrip2.np[0]
    packetToSend['RGB2r'] = red
    packetToSend['RGB2g'] = green
    packetToSend['RGB2b'] = blue

    red, green, blue = rgbStrip3.np[0]
    packetToSend['RGB3r'] = red
    packetToSend['RGB3g'] = green
    packetToSend['RGB3b'] = blue

    red, green, blue = rgbStrip4.np[0]
    packetToSend['RGB4r'] = red
    packetToSend['RGB4g'] = green
    packetToSend['RGB4b'] = blue

    packetToSend['K1'] = kSCheduleList[0][0]
    packetToSend['K2'] = kSCheduleList[1][0]
    packetToSend['K3'] = kSCheduleList[2][0]
    packetToSend['K4'] = kSCheduleList[3][0]
    packetToSend['K5'] = kSCheduleList[4][0]
    packetToSend['K6'] = kSCheduleList[5][0]
    packetToSend['K7'] = kSCheduleList[6][0]
    packetToSend['K8'] = kSCheduleList[7][0]

    return str(packetToSend).replace("'", '"').replace('True', 'true').replace('False','false')

'''
def sendSamplePackets():
    sendLength = int(file_smpPointer(0, sdAvailable))
    packets = file_smpTempLog(1,'', sdAvailable)
    if (sendLength != len(packets)):
        print('Mismatched length - not all packets were saved. Expected ' + str(sendLength) + ', found ' + str(len(packets)))
        ##print(str(file_smpTempLog(3,'',sdAvailable)))
        print('---------------------------------------------------')
    for packet in packets:
        mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/sensors', msg=packet, qos=MQTTqos)
        time.sleep_ms(10)
    file_smpPointer(1, sdAvailable, "0")
    file_smpTempLog(2,'', sdAvailable)
'''


def sendErrorLog():
    global internalSettings
    content = str(file_errLog(1, "", sdAvailable))
    print("Sending Error log HTTP POST")
    if ('uploadLog' not in internalSettings):
        internalSettings['uploadLog'] = 0
    if (internalSettings['uploadLog'] == 0):
        internalSettings['uploadLog'] = 1
        writeFram()
        reply = requests.post("http://airmonitor-utils.terrasls.com/errorLog/?id=AM-" + deviceSettings['device_id'], data=content, headers= { "Content-Type": "text/plain" })
        internalSettings['uploadLog'] = 0
        writeFram()
    else:
        print("Log will timout if upload attempted, clearing error log")
        file_errLog(2, "", sdAvailable)

    print("Done - Sent Error Log, clearing")
    file_errLog(2, "", sdAvailable)


def buildMQTTStatusPacket(details=None, proxyDevice=None):
    global deviceSettings, lteScan

    if 'debugging_messages' in deviceSettings:
        if deviceSettings['debugging_messages']:
            sendErrorLog();

    packetToSend = {}
    if Skyhook: # 2.2.2b
        packetToSend['device_id'] = "SKY-" + deviceSettings['device_id']
    else:
        packetToSend['device_id'] = "AM-" + deviceSettings['device_id']

    packetToSend['up_time'] = int(time.ticks_ms()/1000)
    if proxyDevice:
        packetToSend['device_id'] = proxyDevice
    packetToSend['timestamp'] = time.time()
    if details:
        packetToSend['details'] = details
    json_data = {}
    wifiInfo = False
    if deviceSettings['wifi_enabled']:
        wifiInfo = scanWiFi()
    if lteScan != False and scanSent == False:
        json_data = { "homeMobileCountryCode": lteScan["mobileCountryCode"] ,"homeMobileNetworkCode": lteScan["mobileNetworkCode"], "radioType": "lte", "carrier": "Verizon", "considerIp": "true", "cellTowers": [ lteScan ] }
    if wifiInfo != False:
        json_data['wifiAccessPoints'] = wifiInfo
    conn_data = []
    if wlan.isconnected():
        info = currentWifiInfo()
        packetToSend['wlan_mac'] = info['details']['macAddress']
        packetToSend['wlan_signal_strength'] = info['details']['signalStrength']
        packetToSend['wlan_ssid'] = info['details']['ssid']
    # 2.2.20 if deviceSettings['lte_enabled'] and connectLTE(deviceSettings): # 2.2.4 added settings
    if deviceSettings['lte_enabled'] and connectLTE(deviceSettings, fnWDog, sdAvailable): # IamAlive, sdAvailable): # 2.2.20
        try:
            stopLTE()
            info = infoLTE(deviceSettings['lte_enabled'])
            packetToSend['LTE_signal_strength'] = info['details']['signalStrength']
        except:
            pass
        startLTE()
    if json_data:
        packetToSend['scanInfo'] = json_data
    return str(packetToSend).replace("'", '"').replace('True', 'true').replace('False','false')


''' CF
# uptime, battery, imei, lte scan, version, firmware, fipy-id,
def buildLoRaStatusPacket(batteryVoltage='-', lowPower=False):
    global version, statusStruct, statusStructKey, lteData

    packetToSend = struct.pack(statusStruct, int(time.ticks_ms()/1000), int(''.join(filter(str.isdigit, version))), int(lteData['details']['imei']), int(batteryVoltage*10))
    msg = buildMsg(closestNode['key'], l_topics['status'], data=statusStructKey + packetToSend, shortTime=True)
    return msg
'''
def buildLoRaStatusPacket(lowPower=False):  # CF
    global version, statusStruct, statusStructKey, lteData, battV

    packetToSend = struct.pack(statusStruct, int(time.ticks_ms()/1000), int(''.join(filter(str.isdigit, version))), int(lteData['details']['imei']), int(battV*10))
    msg = buildMsg(closestNode['key'], l_topics['status'], data=statusStructKey + packetToSend, shortTime=True)
    return msg


def customDeepSleep(ms, coldboot = False):   # cf3/12   (ms):
    global tvocBaseline, co2eBaseline # 5b

    # cf3/15
    # if hm3300Available:
    #    hm3300.powerMode(0)
    powerPM(0)

    # cf3/10  if sgp30Available: # 5b  Save VOC baselines
    #    co2eBaseline, tvocBaseline = sgp30.get_baseline("customDeepSleep")
    #    internalSettings['co2_base'] = co2eBaseline # 2.3.10 internalSetting  configurator.writeVRAM('co2_base', co2eBaseline)  2.3.8
    #    internalSettings['tvoc_base'] = tvocBaseline # 2.3.10 internalSetting  configurator.writeVRAM('tvoc_base', tvocBaseline) 2.3.8
    #    # cf3/7  sgp30.soft_reset()

    if wsoAvailable == True:   # 2.2.1b    deviceSettings[' weather_shield_option'] > 0: # 2.2.0b
        wsCommSw.channel(0x00) # disa2.3.10 internalSetting  ble weather shield I2C

    if 'pycom_socket_version' in deviceSettings:
        if deviceSettings['pycom_socket_version'] == 2 or deviceSettings['pycom_socket_version'] == 20: # 2.2.5b
            if DIOAvailable: # 2.2.18
                do_Relay(0,False)

        # 2.2.0b
        if deviceSettings['pycom_socket_version'] == 3 or deviceSettings['pycom_socket_version']  == 21: # 2.2.12b
            print( "Turn OFF power to weathershield.")
            if DIOAvailable: # 2.2.18
                # 2.2.21  DIO.writePort(0x00) # Set P0(Power shield)=0 &  P1(PM)=0
                DIO.shieldOFF() # 2.2.21  power shield OFF
                if coldboot: # cf3/12
                    disableAlive(1)  # cf3/12
                else:
                    disableAlive(0) # 2.2.21   DIO.setDisable() # 2.2.21  DogRelay disabled  1H
                if ms > (60*60*1000): ms = 60*60*1000   # 2.2.21  nap can not be more than 1h

        # 2.3.12
        if SocketVersion == 4:
            if swNetAvailable:
                if coldboot: # cf3/12
                    puIC6.setDogRate(1) #  # cf3/12   1= 0.5sec
                else:
                    puIC6.setDogRate(0) #  0= 14sec
                puIC6.sleepPM()
                puIC6.shieldOFF()

    writeFram() # 2.3.6  save FRAM variables
    if coldboot: # cf3/12
        ms = 5*60*1000 # cf3/12   5min DogRelay will coldreset the board within 2min
        IamAlive() # cf3/12
    wdt.init(ms+90000) # 2.05  disable wdt  60x60sec = 1h
    machine.deepsleep(ms)  # 2.05   sleep for 3600sec = 1 hour


# get configuration
# 8b wdt.feed()
wdt.init(10*60*1000) # 2.2.20  10min     1800000) # 30min
disableAlive() # 2.2.20  1H
deviceSettings = configurator.FLASH_CONFIG



if deviceSettings['ap_enabled'] or machine.reset_cause() == machine.PWRON_RESET:
    setupwebserver = startSettingsAP()
    print("Starting AP b/c enabled or power on reset.")
    rgbStrip.setState("secondary") # violet

oldSettings = deviceSettings
if not 'lost' in deviceSettings:
    configurator.runFlashSetup()
if deviceSettings != oldSettings:
    res = configurator.cliQuestioden('Reboot? (y/n) ', boolean=True, default=True, uppercase=False)
    if res == True:
        customReset()

# 2.2.2b deviceSettings = configurator.FLASH_CONFIG
if version not in deviceSettings or deviceSettings['version'] != version:
        configurator.writeFlashKey('version',version)
if build_date not in deviceSettings or deviceSettings['build_date'] != build_date:
        configurator.writeFlashKey('build_date',build_date)
deviceSettings = configurator.FLASH_CONFIG

if not 'pm_interval' in deviceSettings:
    deviceSettings['pm_interval'] = 45
wdt.init(60*1000) # 2.2.20  60 sec
IamAlive() # 2.2.20  resume DogWatch
disableAlive(1) # 2.2.21   2min
if not deviceSettings['ap_enabled'] and not machine.reset_cause() == machine.PWRON_RESET:
    initWiFi(deviceSettings)

wdt.init(2*60*1000) # 2.2.20 TOREVIEW    5*60*1000)  # 8b give time to GPS to initialize
IamAlive() # 2.2.20   wdt.feed()

coldInit()  # make sure 'internalSettings' is setup before calling this function

needRegister = True
try:
    os.stat('/flash/cert/AM-' + deviceSettings["device_id"] + '.cert.pem')
    os.stat('/flash/cert/AM-' + deviceSettings["device_id"] + '.private.key')
    needRegister = False
except:
    needRegister = True

breakThread = False

wdt.init(2*60*1000) # 2.2.20   TOREVIEW     5*60*1000) # 5min
IamAlive() # 2.2.20  wdt.feed()

print(asciiArt.boldHR)
print("FiPy MAC: {}".format(ubinascii.hexlify(machine.unique_id()).decode()))
print("AM-" + deviceSettings['device_id'] + ".")
if (needRegister):
    print("Not AWS registered.")
# cf3/10 if 'lte_carrier' in deviceSettings:
if deviceSettings['lte_enabled']: # cf3/10
    lteData = infoLTE(deviceSettings['lte_enabled'], deviceSettings['lte_carrier'])
else:
    lteData = {}
eID = None
if 'details' in lteData:
    if 'imei' in lteData['details']:
        print("IMEI: {}\n".format(lteData['details']['imei']))
        if 'iccid' in lteData['details']:
            print("ICCID: " + lteData['details']['iccid'])
    if 'eid' in lteData['details']:
        if 'eid' in lteData['details']:
            eID = lteData['details']['eid']
            print("EID: " + lteData['details']['eid'])


wdt.init(2*60*1000) # 2.2.20      5*60*1000) # 5min
IamAlive() # 2.2.20   wdt.feed()

print(asciiArt.thinHR)
print("'Tab' completion available, if a message is printed over what your typing - it's still there.")
print("To see device settings, type 'deviceSettings'. ")
print("To reset device configuration, type 'resetFlashSettings()'. This will reboot the device after configuration is set.")
print("To change a setting, type 'changeFlashSetting()'.")
print(asciiArt.boldHR)
lteScan = False
#if deviceSettings['lte_enabled']:
    # 2.2.20  lteScan = scanLTE(deviceSettings) # 2.2.4b added device settings
#    lteScan = scanLTE(deviceSettings, fnWDog, sdAvailable) # 2.2.20
#    if lteScan == False: # 5b  LTE not found, disable it, until next reset
#        deviceSettings['lte_enabled'] = False  # 5b
#print("lteScan: " + str(deviceSettings['lte_enabled'])) # 5b


wdt.init(60*1000) # 2.2.20   65000) # 8b
IamAlive() # 2.2.20   wdt.feed()


# 2.2.4b
disableAlive()  # 1H    2.2.20  wdt.init(1800000)
print( "If you want to run tests on the hardware, now is the time to break the execution of the code (Ctrl C)")
print( "Functions are: tstWind(), testWeatherShieldAD(), calibCO2(), calibC1() ....")
# DO NOT REMOVE !!!
for x in range(10):  # allow 10sec to reset or reflash
    time.sleep(1)
    print('.', endln='')
IamAlive() # 2.2.20  resume Dog Watch
disableAlive(1) # 2.2.21  2min
sgp30.get_baseline("\nCal prompt")



IamAlive() # 2.2.20   wdt.feed() # 8b
rgbStrip.setState("info") # blue
rgbStrip.setState("success") # green

# start ap config
IamAlive() # 2.2.20  wdt.feed() # 8b

def stopSettingsAP():
    global setupwebserver, apTimer
    print("Stopping AP.")
    swNet_led(False)
    setupwebserver.Stop()
    disconnectWiFi()
    initWiFi(deviceSettings)
    apTimer.stop()
    apTimer.reset()

IamAlive() # 2.2.20  wdt.feed() # 8b
if deviceSettings['lora_enabled']:
    initLoRa()

timeSet = False

def setRTC():
    global timeSet

    if not timeSet:
        print("Fetching UTC...", endln="")
        timeout = 0
        oldTime = time.time()
        # 2.2.18  fails to get an answer at boot    rtc.ntp_sync("time.nist.gov")
        # rtc.ntp_sync("time-a-b.nist.gov") # 2.2.18  fails repeat
        # rtc.ntp_sync("pool.ntp.org") # 2.2.18 restore original code
        rtc.ntp_sync("1.amazon.pool.ntp.org") # 2.2.20  Eric

        time.sleep(3)
        while timeout < 10:
            IamAlive() # 2.2.20
            time.sleep(1)
            if rtc.synced(): # if ( rtc.synced() == True):  # fail ?
                # 2.2.20 print("Real Time Clock is set.")
                scratch = "Real Time Clock is set" # 2.2.20
                file_errLog(0, scratch, sdAvailable) # 2.2.20
                print("Current time: " + str(rtc.now()))
                if (pcf8523Available): # 2.2.28
                    pcf8523.datetime(rtc.now()) # questionable?
                timeSet = True
                rgbStrip.blinkState("success",2,150) # 2x fast green
                break
            timeout += 1
        if not rtc.synced():
            rgbStrip.blinkState("error",2,150) # 2x fast red
            # 2.2.20  print("Unable to sync time")
            timeSet = False
            scratch = "Unable to sync time" # 2.2.20
            file_errLog(0, scratch, sdAvailable)
            if (pcf8523Available): # 2.2.28
                rtc.init(pcf8523.datetime())
                timeSet = True
                scratch = "using onboard rtc" # 2.2.20
                file_errLog(0, scratch, sdAvailable) # 2.2.20
            print("To set time manually type: 'setTime((year, month, day, hour, min, second))'")


def setTime(tup): # (year, month, day, hour, min, second)
    global timeSet
    if (pcf8523Available): # 2.2.28
        pcf8523.datetime(tup)
    rtc.init(tup) # the parameters  microsec and timezone are not required  !!!
    timeSet = True
    print("Time set to: " + string(rtc.now()))


# establish network state
internetConnected = False
mqttConnected = False
loRaConnected = False
mqttClient = MQTTClient("AirMonitor", "a1njj292w2vjt1-ats.iot.us-west-2.amazonaws.com", port=8883, ssl=True, ssl_params={"certfile": "/flash/cert/global.cert.pem","keyfile": "/flash/cert/global.private.key","ca_certs": "/flash/cert/root-CA.crt"})
offlineTrigger = False
sleepTimer = 60
scanSent = False
configSent = False
## MAIN LOOP
loopCounter = 0
rgbStrip.setState("off")
if apTimer.read() > 0:
    swNet_led(True)


wdt.init(2*60*1000)  # 8b  2min  within the main loop before WatchDog
IamAlive() # 2.2.20  wdt.feed()

print("Starting main loop.\n\n")

''' 2.2.26
def increaseSendLength():
    sendLength = file_smpPointer(0, sdAvailable)
    if (sendLength == False or sendLength == ''):
        file_smpPointer(1,sdAvailable, "0")
        sendLength = 0;
    sendLength = int(sendLength)
    sendLength = sendLength + 1
    file_smpPointer(1,sdAvailable, str(sendLength))
'''

readIdxfile_smpLog = 0 # 2.2.26 initialized in mainLoop

def sendSamplePackets(): # 2.2.26
    global readIdxfile_smpLog

    if sdAvailable:
        try:
            f = open('/sd/smpLog.txt', 'r')
            f.seek(readIdxfile_smpLog)
            expectedPacketCount = int(deviceSettings['send_interval'] / deviceSettings['sample_interval'])
            if expectedPacketCount > 0:
                print('sendSamplePackets #' + str(expectedPacketCount) + ' - Idx: ' + str(readIdxfile_smpLog) + " &")
                for x in range(expectedPacketCount):
                    packet = f.readline()
                    print(packet, debugging=True)
                    mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/sensors', msg=packet, qos=MQTTqos)
                    time.sleep_ms(250)
                    readIdxfile_smpLog += len(packet)
                #D1 = f.tell()
                #if (readIdxfile_smpLog == D1):  print(' All was sent &')

        except Exception as e:
            file_errLog(0, "Failed to send packets over MQTT - " + str(e), sdAvailable)

        if f: f.close()



#internetConnected = checkNetworks()
#if ota != None:
#    print("Checking for OTA...")
#    print("Connected to server.")
#    print("Attempting to update...")
#    ota.update()

apButtonCounter = 0
sgp30.get_baseline("mainLoop")  # cf3/7


def mainLoop(id): # CF
    global sdAvailable, readIdxfile_smpLog # 2.2.10b
    global battV, internetConnected  # CF
    global loopCounter, mqttConnected, loRaConnected, mqttClient, distanceToGateway, offlineTrigger, sleepTimer, breakThread, configSent, needRegister
    global rxjson, wss, apTimer, apButtonCounter # 2.2.17

    samplingMsg = "\nSampling."
    print('\n'+asciiArt.thinHR)
    readIdxfile_smpLog = file_smpLog(3, "", sdAvailable) # 2.2.26

    while True:
        if breakThread:
            print('mainLoop stopped')
            wdt.init(3600*1000) # ms   1h     8b
            break

        loopCounter = loopCounter + 1
        IamAlive()  # 2.2.18  wdt.feed()

        if swNet_read() and apTimer.read() == 0:
            apButtonCounter = apButtonCounter + 1
        else:
            apButtonCounter = 0

        if apButtonCounter == 2:
            print("Starting AP b/c button hold.")
            startSettingsAP()



        msLaps = sampleSensors(samplingMsg) # sample sensors every second nessary to maintain baselines and for battery monitoring
        sleepLaps = int(1000 - msLaps) # 2.2.1b
        if sleepLaps < 100: sleepLaps = 100 # 2.2.1b
        #print("Sampling (ms):" + str(msLaps) + " Sleep (ms): " + str(sleepLaps)) # 2.2.1b

        samplingMsg = "."
        if deviceSettings['light_blinks']:
            rgbStrip.blinkState("success", 1, sleepLaps / 2) # 2.2.4b   2.2.1b   1000 - 560) # 2.2.0b   500) # 1x green
            # 2.2.0b time.sleep_ms(500)
        else:
            time.sleep_ms(sleepLaps) # 2.2.1b   1000 - 560)  # CF period of the loop about 1sec


        # 2.2.2b
        if mqttConnected:
            mqttClient.check_msg()  # acquire and process messages
            # if (rxjson != ''): # 2.2.17
            #    interpret_rxjson() # 2.2.17   see printSubmsg()
            #    rxjson = '' # 2.2.17


        # 4b   stop checking the radios all the time
        if not ((loopCounter == 5) or (loopCounter % deviceSettings['sample_interval'] == 0)):
            continue


        if (apTimer.read() > 600 and deviceSettings['ap_enabled'] == False): ## 10 min time out
            stopSettingsAP()

        if (len(wss) > 0):
            apTimer.reset()

        # coms checks
        if deviceSettings['offline_mode'] == False:
            internetConnected = checkNetworks() # checks internet is still connected every loop -- if not; it try to reconnect
        # print("internetConnected:" + str(internetConnected)) # 4b
        if internetConnected == True: # 4b if internetConnected:
            setRTC()
            distanceToGateway = 1
            oldmqttState = mqttConnected
            mqttConnected = checkMQTT() # check mqtt connection is connected -- if not trys to connect

            if mqttConnected != oldmqttState and mqttConnected == True and deviceSettings['lora_enabled']:
                sendPing() # LoRa

        else:
            if distanceToGateway == 1:
                distanceToGateway = 0
            mqttConnected = False


        if loopCounter % ( 5 * deviceSettings['sample_interval']) == 0 and deviceSettings['lora_enabled']: # 30 means 3 min
            loRaConnected = checkLoRa() # check that LoRa relay or gateway devices are nearby -- if not send ping
            # CF if loRaConnected = true it is cause 'lora_enabled' = true !!!


        # no one to listen to data no point in sampling...? reboot in 10min    # 4b  30 min.
        if not loRaConnected and mqttConnected == False and deviceSettings['offline_mode'] == False: # standard/teal carriers can take up to 24 hours to properly connect
            rgbStrip.blinkState("error", 3, 500) # 3x red
            if offlineTrigger == False:
                sleepTimer = 4 # 4b    * 60 # CF 3min     6 * deviceSettings['sample_interval']
                print("Warning: No comms connected !!!") # 4b  , waiting {} minutes then going to sleep for 30 minutes if no connections found.".format(sleepTimer / 60))
                offlineTrigger = True
                # CF  if not loRaConnected and offlineTrigger and deviceSettings['lora_enabled']:
            else:
                sleepTimer = sleepTimer - 1
                if sleepTimer == 2:
                    lte.reset()
                if sleepTimer <= 0:
                    # 2.2.20 print("No comms connected, going to sleep for 10 minutes") # 4b
                    scratch = "No comms connected, going to sleep for 5 minutes, tried 2 times for 4 minutes each, reset and tried again 2 more times." # 2.2.20
                    file_errLog(0, scratch, sdAvailable) # 2.2.20
                    wdt.init(999999); ## try to hardware reboot
                    time.sleep(420);
                    customDeepSleep(int(5*60*1000)) # 4b    3600 / 2 * 1000))  # 1/2Hour
        else:
            offlineTrigger = False

        # send config packet once at reset and on MQTT only
        # send status packet once per day on both MQTT and LORA
        if loopCounter < 43200: # on start of new loop (day) send status
            rgbStrip.blinkState("primary",2,250) # 2x fast yellow
            if mqttConnected and needRegister == False:
                try:
                    if configSent == False:
                        packetToSend = buildMQTTConfigPacket()
                        if not needRegister:
                            mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/config', msg=packetToSend, qos=MQTTqos) # 2.2.0b  0)
                            configSent = True
                            print ("Send config - " + str(loopCounter)) # 2.2.2b  print("Sending config over MQTT.")
                            print(packetToSend, debugging=True)
                        # time.sleep_ms(1000) # 2.2.0b


                        packetToSend = buildMQTTStatusPacket(details=resetMessage) # 9b debug trace
                        if not needRegister:
                            mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/status', msg=packetToSend, qos=MQTTqos) # 2.2.0b   0)
                            print ("Send status - " + str(loopCounter)) # 2.2.2b print("Sending status over MQTT.")
                            print(packetToSend, debugging=True)
                        if Skyhook:  # 2.2.2b
                            packetToSend = buildMQTTSkyHookPacket()
                            if not needRegister:
                                mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/status', msg=packetToSend, qos=MQTTqos)
                                print ("Send SH status - " + str(loopCounter)) # 2.2.2b print("Sending Skyhook status over MQTT.")
                                print(packetToSend, debugging=True)
                            if loRaConnected and not mqttConnected: # cf and deviceSettings['lora_enabled']:
                                packetToSend = buildLoRaStatusPacket()
                                sendLoRa(packetToSend)
                                print("Sending status over LoRa.")
                                configSent = True
                except Exception as e: # 2.2.9b
                    file_errLog(0, "Failed to send packet - " + str(e), sdAvailable) # 2.2.9b    print("Failed to send packet over MQTT.")
                    rgbStrip.blinkState("warning",2,250) # 2x fast orange
                    mqttClient.disconnect()
                    mqttConnected = False

            wdt.feed() # 8b

        # CF combined code for sending samples and status packet of data
        # CF over MQTT send both samples and status in case of lowbatt
        # CF send either status or samples over LORA never both
        if loopCounter % deviceSettings['sample_interval'] == 0: # send sample to cloud interval with set methods
            samplingMsg = "\nSampling."
            samplePacket = buildSamplePacket()

            if (deviceSettings['ap_enabled'] or len(wss) > 0):
                for user in wss:
                    packetToSendWs = {}
                    ## over complicated its not that much data send it all!
                    #for sub in wss[user]['subscriptions']:
                    #    if (sub in samplePacket):
                    #        packetToSendWs[str(sub)] = str(samplePacket[sub])
                    packetToSendWs = samplePacket
                    if packetToSendWs:
                        wss[user]['ws'].SendTextMessage('{ "sensors": ' + jsonToStr(packetToSendWs) + '}')

            packetToSend = buildMQTTSamplePacket(samplePacket)
            file_smpLog(0, packetToSend, sdAvailable)
            # 2.2.26 print ("Save packet - Loop:" + str(loopCounter))
            #file_smpTempLog(0, packetToSend, sdAvailable)
            #increaseSendLength()
            #if int(file_smpPointer(0, sdAvailable)) > (deviceSettings['send_interval']/deviceSettings['sample_interval']):
                #print ("Send sensors (tmp length) - " + str(loopCounter)) # 2.2.2b print("Sending sample over MQTT.")
                #sendSamplePackets()
            print(samplePacket, debugging=True)

        # if loopCounter % 3600 == 0: # 2.3.6 #2.4.4
            #writeFram()

        if loopCounter % deviceSettings['send_interval'] == 0: # send sample to cloud interval with set methods
            # 2.2.26 packetToSend = buildMQTTSamplePacket(samplePacket) # log sample to sd regardless of connection status
            # 2.2.26 file_smpLog(0, packetToSend, sdAvailable) # 2.2.10b
            # print ("Save packet - Loop:" + str(loopCounter)) # 2.2.26
            # print('\nLogging sample.', debugging=True)
            if mqttConnected and not needRegister:
                try:
                    ''' 2.2.26
                    # 2.2.0b   0)
                    print ("Send sensors - " + str(loopCounter)) # 2.2.2b print("Sending sample over MQTT.")
                    mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/sensors', msg=packetToSend, qos=MQTTqos)
                    print(packetToSend, debugging=True)
                    # time.sleep_ms(1000) # 2.2.0b
                    '''

                    sendSamplePackets() # 2.2.6
                    if battV < deviceSettings['low_battery_voltage_threshold'] and battV > 1: # CF
                        packetToSend = buildMQTTStatusPacket(details="Low power: Powering down for 30min.") # 2.2.0b
                        if not needRegister:
                            mqttClient.publish(topic='AM/AM-' + deviceSettings['device_id'] + '/status', msg=packetToSend, qos=MQTTqos) # 2.2.0b   0) # 9b change destination
                    rgbStrip.blinkState("success",2,250)  # 2x fast green

                except Exception as e: # 2.2.9b
                    file_errLog(0, "Failed to send over MQTT - " + str(e), sdAvailable) # 2.2.9b    print("Failed to send over MQTT.")
                    rgbStrip.blinkState("warning",2,250) # 2x fast orange
                    mqttConnected = False


            if loRaConnected and not mqttConnected:  # cf  and deviceSettings['lora_enabled']:
                if battV < deviceSettings['low_battery_voltage_threshold'] and battV > 1: # CF
                    packetToSend = buildLoRaStatusPacket(lowPower=True)
                else:
                    packetToSend = buildLoRaSamplePacket(samplePacket)
                print("Sending packet over LoRa.")
                print(packetToSend, debugging=True)
                sendLoRa(packetToSend) # sends to closest gateway
                rgbStrip.blinkState("warning",2,250)  # 2x fast orange


            # if battery is too low to oporate go to deep sleep for 1h (reboot in 1h)
            if battV < deviceSettings['low_battery_voltage_threshold'] and battV > 1: # CF
                # 2.2.20 print("Battery low ({:.2f}V): Going to sleep for 30min...".format(battV))
                scratch = "Battery low ({:.2f}V): Going to sleep for 30min...".format(battV) # 2.2.20
                file_errLog(0, scratch, sdAvailable) # 2.2.20
                # Disable the next line when debuging
                customDeepSleep(30*60*1000)  # 8b 30min instead of 1Hour, it uses less power to shutdown in boot.py

            wdt.feed() # 8b


        if loopCounter >= 86400: # reset counter about once a day assuming each loop takes 1 second
            loopCounter = 0
            configSent = False


def stopRun(): # 2.2.24b
    global breakThread  # 2.2.27

    wdt.init(3600*1000) # 1800000)
    disableAlive()
    IamAlive()
    breakThread = True
    time.sleep(3)
    writeFram() # 2.3.6  save FRAM variables


# 2.3.10  for debug
# FG = MAX17048(0x36, i2c=I2C1, traceDebug=True, sdAccess=sdAvailable)
# FG.begin()
# FG.read_vcell()

# µPing (MicroPing) for MicroPython
# copyright (c) 2018 Shawwwn <shawwwn1@gmail.com>
# License: MIT

# Internet Checksum Algorithm
# Author: Olav Morken
# https://github.com/olavmrk/python-ping/blob/master/ping.py
# @data: bytes
def checksum(data):
    if len(data) & 0x1: # Odd number of bytes
        data += b'\0'
    cs = 0
    for pos in range(0, len(data), 2):
        b1 = data[pos]
        b2 = data[pos + 1]
        cs += (b1 << 8) + b2
    while cs >= 0x10000:
        cs = (cs & 0xffff) + (cs >> 16)
    cs = ~cs & 0xffff
    return cs


def ping(host, count=6, timeout=5000, interval=10, quiet=False, size=64):
    import utime
    import uselect
    import uctypes
    import usocket
    import ustruct
    import uos
    global currentPingTime

    # prepare packet
    assert size >= 16, "pkt size too small"
    pkt = b'Q'*size
    pkt_desc = {
        "type": uctypes.UINT8 | 0,
        "code": uctypes.UINT8 | 1,
        "checksum": uctypes.UINT16 | 2,
        "id": (uctypes.ARRAY | 4, 2 | uctypes.UINT8),
        "seq": uctypes.INT16 | 6,
        "timestamp": uctypes.UINT64 | 8,
    } # packet header descriptor
    h = uctypes.struct(uctypes.addressof(pkt), pkt_desc, uctypes.BIG_ENDIAN)
    h.type = 8 # ICMP_ECHO_REQUEST
    h.code = 0
    h.checksum = 0
    h.id[0:2] = uos.urandom(2)
    h.seq = 1

    # init socket
    sock = usocket.socket(usocket.AF_INET, usocket.SOCK_RAW, 1)
    sock.setblocking(0)
    sock.settimeout(timeout/1000)
    try:
        addr = usocket.getaddrinfo(host, 1)[0][-1][0] # ip address
    except IndexError:
        not quiet and file_errLog(0, "Could not determine the address of" + host, sdAvailable)
        return None
    sock.connect((addr, 1))
    not quiet and file_errLog(0, "PING %s (%s): %u data bytes" % (host, addr, len(pkt)), sdAvailable)

    seqs = list(range(1, count+1)) # [1,2,...,count]
    c = 1
    t = 0
    n_trans = 0
    n_recv = 0
    finish = False
    while t < timeout:
        if t==interval and c<=count:
            # send packet
            h.checksum = 0
            h.seq = c
            h.timestamp = utime.ticks_us()
            h.checksum = checksum(pkt)
            if sock.send(pkt) == size:
                n_trans += 1
                t = 0 # reset timeout
            else:
                seqs.remove(c)
            c += 1

        # recv packet
        while 1:
            socks, _, _ = uselect.select([sock], [], [], 0)
            if socks:
                resp = socks[0].recv(4096)
                resp_mv = memoryview(resp)
                h2 = uctypes.struct(uctypes.addressof(resp_mv[20:]), pkt_desc, uctypes.BIG_ENDIAN)
                # TODO: validate checksum (optional)
                seq = h2.seq
                if h2.type==0 and h2.id==h.id and (seq in seqs): # 0: ICMP_ECHO_REPLY
                    t_elasped = (utime.ticks_us()-h2.timestamp) / 1000
                    ttl = ustruct.unpack('!B', resp_mv[8:9])[0] # time-to-live
                    n_recv += 1
                    not quiet and file_errLog(0, "%u bytes from %s: icmp_seq=%u, ttl=%u, time=%f ms" % (len(resp), addr, seq, ttl, t_elasped), sdAvailable)
                    currentPingTime = { "value": t_elasped, "unit": "ms" }
                    seqs.remove(seq)
                    if len(seqs) == 0:
                        finish = True
                        break
            else:
                break

        if finish:
            break

        utime.sleep_ms(1)
        t += 1

    # close
    sock.close()
    ret = (n_trans, n_recv)
    not quiet and file_errLog(0,"%u packets transmitted, %u packets received" % (n_trans, n_recv), sdAvailable)
    return (n_trans, n_recv)

machine.info()  # 8b
print("OS name:" + str(uos.uname()), debugging=True)  # 6b
# print("network config:" + str(wlan.ifconfig()), debugging=True) # 6b

# loopCounter = 0
# wdt.init(120*1000)
# def mainLoop():
#     global loopCounter
#     while True:
#         loopCounter = loopCounter + 1
#         wdt.feed()
#         msLaps = sampleSensors() # sample sensors every second nessary to maintain baselines and for battery monitoring
#         sleepLaps = int(1000 - msLaps) # 2.2.1b
#         if sleepLaps < 100: sleepLaps = 100
#         time.sleep_ms(sleepLaps)
#
#         if loopCounter % 30 == 0:
#             print(buildSamplePacket())
#
# mainLoop()
_thread.start_new_thread(mainLoop, (1,)) # 6b disabled for debug
# wdt.init(1800000) # 6b   DBUG   for debug only
