-------------------------------------------------------------------------------
-- Copyright (c) 2015 Sierra Wireless. All rights reserved.
--
-- Contributors:
--     Randall Pfeifer - initial implementation
--     Tom Ho          - extend to discover/report proprietary vehicle MSCIIDs (AVTA-105)
--
--	Main application module.
-------------------------------------------------------------------------------

-- ----------------------------------------------------------------------------
-- REQUIRES
-- ----------------------------------------------------------------------------

local sched         = require "sched"
local log           = require "log"
local devicetree    = require "devicetree"
local airvantage    = require "airvantage"
local persist       = require "persist"
local timer         = require "timer"
local serial        = require 'serial'
local os            = require "os"
local psignal       = require "posixsignal"
local string        = require "string"
local bit           = require "bit32"
local pkginfo       = require "pkginfo"
local url           = require 'socket.url'
local uptime        = require 'sched.timer.core'
local lfs           = require 'lfs'
local json          = require 'json'

-- application specific code module constructors
local _bbem = require 'bbem'
local _BnB = require 'BnB'
local _mqtt = require 'mqtt'
local _telemetry = require 'telemetry'
local _transport = require 'transport'

-- Primary code objects
local mqtt
local transport
local BnB
local telemetry
local wanstate = require 'wanstate'

-- ----------------------------------------------------------------------------
-- CONSTANTS
-- ----------------------------------------------------------------------------

-- Set to false to disable debugging interface --
local DEBUG = true

local APP_NAME = "AVTA"
local DEFAULT_MQTT_PORT = 1883
local QOS = 1
local TIMEOUT = 10000
local DEFAULT_PASSWORD = "Sierra-123"
local MQTT_ASSET_NAME = "atp"
local DEVICE_ID = ""
local MQTT_DATA_PATH = ""
local ATP_VERSION = "2.1"

local TELEM_CFG_FILE_TITLE    = nil -- Store Active Telemetry Cfg File's TITLE
local TELEM_CFG_FILE_REVISION = nil -- Store Active Telemetry Cfg File's REVISION
local TELEM_CFG_FILE_SIGNAL_NAME_TBL = {} -- { aleos.<msciid>, <datum name>  } pair
local TELEM_CFG_FILE_SIGNAL_UNIT_TBL = {} -- { aleos.<msciid>, <datum units> } pair
local TELEM_CFG_FILE_SIGNAL_REGISTERED_USERID_TBL = {} -- { aleos.<msciid>, <userid returned by devicetree.register()> } pair
local TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE = {} -- { <datum name>, <datum vaue> } pair; keep cache until time to publish
local TELEM_CFG_FILE_SIGNAL_LAST_PUBLISH  =  0 -- time at which TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE was last published to ALMS

-- Scan Tool status
local TELEM_SCAN_TOOL_STATUS = nil -- 0 = Not present, 1 = Scan tool present causing delays, 2 = Scan tool present preventing TX

local CONFIG_ASSET_NAME = "avta_config"

-- File name for mqtt queue persistange
local MQTT_PERSIST_NAME = "mqtt_data"

-- Convert interface DPI from wanstate to index
local DPItoIndex = {
	[0] = "none",
	[0x30200000] = "cell1",
	[0x30100000] = "wifi1",
	[0x09000000] = "eth1",	
	[0x09010000] = "eth2",
	[0x09030000] = "eth4",

}

-- Table to re-name data parameters from internal name to external name
-- The name on the left is the internal name of the parameter,
-- and corresponds with it's configuration (if it has one).
-- The name on the right in the parameter name as sent via ATP.
local rename = {
	-- These parameters have confuration settings
	vehicle_speed						= "vspd",
	engine_speed						= "espd",
	throttle_position					= "tpos",
	odometer							= "odo",
	fuel_level							= "flvl",
	engine_coolant_temp					= "ect",
	ignition_status						= "igns",
	mil_status							= "mil",
	fuel_rate							= "frat",
	battery_voltage						= "bat",
	pto_status							= "pto",
	seatbelt_fastened					= "sblt",
	misfire_monitor						= "mfm",
	fuel_system_monitor					= "fsm",
	comprehensive_component_monitor		= "ccm",
	catalyst_monitor					= "cm",
	heated_catalyst_monitor				= "hcm",
	evaporative_system_monitor			= "evsm",
	secondary_air_system_monitor		= "sasm",
	ac_system_refrigerant_monitor		= "asrm",
	oxygen_sensor_monitor				= "osm",
	oxygen_sensor_heater_monitor		= "oshm",
	egr_system_monitor					= "esm",
	brake_switch_status					= "bss",
	ambient_air_temperature				= "atmp",
	trip_odometer						= "todo",
	trip_fuel_consumption				= "tfc",
	distance_since_dtc_cleared			= "dtcd",
	transmission_fluid_temperature		= "tft",
	oil_life_remaining					= "oill",
	tire_pressure_monitoring_status		= "tpms",
	tire_pressures						= "tprs",
	supported_pids						= "pids",
	vin									= "vin",
	dtcs								= "dtcs",
	engine_run_time						= "ert",	-- was renamed to eng_run_time

	dreckon_sensor_calibration			= "drc",	-- was renamed to dre_sen_cal
	heartbeat							= "hb",
	wanlinktxbytes						= "wtx",
	wanlinkrxbytes						= "wrx",

	-- gwinfo
	gwimei								= "imei",

	-- event names
	evt_acceleration_incident_start		= "ainc_strt",
	evt_acceleration_incident_end		= "ainc_end",
	evt_deceleration_incident_start		= "dinc_strt",
	evt_deceleration_incident_end		= "dinc_end",
	evt_cornering_incident_start		= "cinc_strt",
	evt_cornering_incident_end			= "cinc_end",
	evt_dreckon_sensor_calibration		= "drc_updt",
	

	-- These parameters have no direct configuration.

	gps_latitude						= "glat",
	gps_longitude						= "glon",
	gps_heading							= "ghed",
	gpsspeed							= "gspd",
	gpsstate							= "gstt",
	gpssatnum							= "gsat",
	gpsquality							= "gqal",
	incident_id							= "iid",

	cell_rssi							= "rssi",
	lte_rsrp							= "rsrp",
	lte_rsrq							= "rsrq",
	lte_sinr							= "sinr",

	acceleration_incident_duration		= "incd",	-- was renamed to acc_inc_dur
	deceleration_incident_duration		= "incd",	-- was renamed to dec_inc_dur
	cornering_incident_duration			= "incd",	-- was renamed to cor_inc_dur
	acceleration_incident_peak			= "incp",
	deceleration_incident_peak			= "incp",
	cornering_incident_peak				= "incp",
	acceleration_incident_average		= "inca",
	deceleration_incident_average		= "inca",
	cornering_incident_average			= "inca",
	acceleration_incident_initial		= "inci",
	deceleration_incident_initial		= "inci",
	cornering_incident_initial			= "inci",
	acceleration_incident_threshold		= "inct",	-- was renamed to acc_inc_thresh
	deceleration_incident_threshold		= "inct",	-- was renamed to dec_inc_thresh
	cornering_incident_threshold		= "inct",	-- was renamed to cor_inc_thresh
	wanstatus							= "wans",
	wanipaddress						= "wana",	-- was renamed to wanip
	linkinfo							= "link",
	primarywan							= "pwan",
	vpnstatus							= "vpns",
	carrier                             = "crr",
	cellrat                             = "ct",
	celllteband                         = "lteb",
	wifiwan                             = "wwan",
	wifissid                            = "wssid",
	gpioinput							= "gpi",
	error								= "err",
	warning								= "wrn",
	info								= "inf",
	shutdown							= "shut",	-- was renamed to gwshutdown
	startup								= "strt",	-- was renamed to gwstartup
}

-- Table to associate MP70 accelerometer incidents with dev tree params
-- start events and end events share structure, so group them
local vbistartparms = {
	acceleration_incident_start = {name = "acceleration_incident", csm = "system.vehicle.accel.event_start"},
	deceleration_incident_start = {name = "deceleration_incident", csm = "system.vehicle.brake.event_start"},
	cornering_incident_start = {name = "cornering_incident", csm = "system.vehicle.side.event_start"}
}
local vbiendparms = {
	acceleration_incident_end = {name = "acceleration_incident", csm = "system.vehicle.accel.event_end"},
	deceleration_incident_end = {name = "deceleration_incident", csm = "system.vehicle.brake.event_end"},
	cornering_incident_end = {name = "cornering_incident", csm = "system.vehicle.side.event_end"}
}

-- Table to associate MP70 accelerometer incidents with associated threshold values
local vbithresh = {
	acceleration_incident = "system.vehicle.accel.threshold",
	deceleration_incident = "system.vehicle.brake.threshold",
	cornering_incident = "system.vehicle.side.threshold"
}

-- ----------------------------------------------------------------------------
-- VARIABLES
-- ----------------------------------------------------------------------------

-- flag to indicate need to update mqtt connection
local restart_mqtt = false

-- current message verbosity level (1=error, 2=error+warn, 3=error+warn+info) for info messages
local message_level = 0

-- table of active incidents/IDs
local active_incident_ids = {}

local SW_VERSION

-- These are set to point to either ignition input or I/O input in device tree
local IGNITION_SENSE_LINE
local IGNITION_SENSE_TIME

-- validators for common parameters
local function validate_posnum(pname, pvalue)
	if tonumber(pvalue) and tonumber(pvalue) >= 0 then
		return true
	end
	return false
end

local function validate_string(pname, pvalue)
	if type(pvalue) == "string" then
		return true
	end
	return false
end
-- system options default values, validator (mandatory, return true if valid), and optional action handlers for changed setting
local default_system_options = {
	message_level = {
		value = 0,
		validate = validate_posnum,
		handler =
			function(pname, pvalue)
				message_level = tonumber(pvalue)
			end
	},
	mqtt_url = {
		value = "",
		validate = validate_string,
		handler = validate_string,
			function(pname, pvalue)
				restart_mqtt = true
			end,
	},
	mqtt_uid = {
		value = "",
		validate = validate_string,
		handler =
			function(pname, pvalue)
				restart_mqtt = true
			end,
	},
	mqtt_passwd = {
		value = "",
		validate = validate_string,
		handler =
			function(pname, pvalue)
				restart_mqtt = true
			end,
	},
	mqtt_certpath = {
		value = "AVCERT",
		validate = validate_string,
		handler =
			function(pname, pvalue)
				restart_mqtt = true
			end,
	},
	mqtt_truststore = {
		value = "",
		validate = validate_string,
		handler =
			function(pname, pvalue)
				restart_mqtt = true
			end,
	},
	mqtt_keystore = {
		value = "",
		validate = validate_string,
		handler =
			function(pname, pvalue)
				restart_mqtt = true
			end,
	},
	mqtt_privatekey = {
		value = "",
		validate = validate_string,
		handler =
			function(pname, pvalue)
				restart_mqtt = true
			end,
	},
	sys_log_level = {
		value = "INFO",
		validate =
			function(pname, pvalue)
				local valid = {NONE=true,ERROR=true,WARNING=true,INFO=true,DETAIL=true,DEBUG=true,ALL=true}

				return valid[pvalue]
			end,
		handler =
			function(pname, pvalue)
				log.setlevel(pvalue, APP_NAME)
			end
	},
	sys_tx_persist_rate = {
		value = 30,
		validate = validate_posnum,
		handler =
			function(pname, pvalue)
				transport:configure {persistrate = pvalue}
			end
	},
	sys_tx_persist_threshold = {
		value = 5,
		validate = validate_posnum,
		handler =
			function(pname, pvalue)
				transport:configure {persistthresh = pvalue}
			end
	},
	sys_mqtt_connect = {
		value = 10,
		validate = validate_posnum,
		handler =
			function(pname, pvalue)
				mqtt:configure {connect_wait = pvalue}
			end
	},
	sys_mqtt_send = {
		value = 10,
		validate = validate_posnum,
		handler =
			function(pname, pvalue)
				mqtt:configure {send_wait = pvalue}
			end
	},
	sys_tx_latency = {
		value = 5,
		validate = validate_posnum,
		handler =
			function(pname, pvalue)
				transport:configure {latency = pvalue}
			end
	},
	sys_tx_maxsend = {
		value = 256,
		validate = validate_posnum,
		handler =
			function(pname, pvalue)
				transport:configure {maxsend = pvalue}
			end
	},
	sys_tx_maxqueue = {
		value = 10000,
		validate = validate_posnum,
		handler =
			function(pname, pvalue)
				transport:configure {maxqueue = pvalue}
			end
	},
	sys_tx_queuetrail = {
		value = 500,
		validate = validate_posnum,
		handler =
			function(pname, pvalue)
				transport:configure {queuetrail = pvalue}
			end
	}
}

-- Empty file created to indicate device boot has been processed
local bootflagfile = "/tmp/RA/avtabootflag"

-- Table of used MSCI parameter definitions (from MSCIIDs.h)
local msciid = {
	STS_POWERIN						= "aleos.266",
	STS_SYSTEM_BOOTS				= "aleos.302",
	STS_MODEM_SENT_PERSIST			= "aleos.10283",
	STS_MODEM_RECV_PERSIST			= "aleos.10284",
	CFG_LOW_PWR_IGNITION_TIMER		= "aleos.11131",
	CFG_LOW_PWR_IO_LINE_TIMER		= "aleos.11143",
	STS_TIME_VALID					= "aleos.55010",
	MP70_VEH_BEHAVE_EN				= "aleos.10995",
	STS_DRECKON_SENSOR_CALIBRATED	= "aleos.10996",
	STS_LTE_SINR                    = "aleos.10211",
	CANBUS_ENABLED_IGN_SOURCE       = "aleos.59080",
	CFG_AAF_RESERVE_SERIAL_PORT		= "aleos.10120",
    STS_TELEM_CFG_FILE_TITLE        = "aleos.59390",
    STS_TELEM_CFG_FILE_REVISION     = "aleos.59391",
	STS_TELEM_SCAN_TOOL_STATUS      = "aleos.59389",
	STS_WAN_IFACE_IP4               = "aleos.10438",
	STS_CELL_ROAM_OPERATOR          = "aleos.780",
	STS_CELL_RAT                    = "aleos.264",
	STS_CELL_BAND                   = "aleos.671",
	STS_WIFI_MODE_A                 = "aleos.10451",
	STS_WIFI_MODE_B                 = "aleos.55782",
	STS_WIFI_A_AVAILABLE_AP         = "aleos.10452",
	STS_WIFI_B_AVAILABLE_AP         = "aleos.55785"
}

-- Associate dev tree gpio to bit value
local gpiobits = {
	["system.aleos.io.in1"] = 1,
	["system.aleos.io.in2"] = 2,
	["system.aleos.io.in3"] = 4,
	["system.aleos.io.in4"] = 8,
	["system.aleos.io.in5"] = 16
}

-- ----------------------------------------------------------------------------
-- HELPER FUNCTIONS
-- ----------------------------------------------------------------------------

-- Debugging routine to display bytes in a string
function showdata(str)
	if type(str) ~= "string" then
		return ""
	end

	local s = ""
	local i = 1
	while(string.byte(str,i)) do
		s = s..(string.format("%02x ",string.byte(str,i)))
		i = i + 1
	end
	return s
end

-- Remap some parameter names prior to sending. These names were
-- shortened to reduce bandwidth usage (an bit). Do this via table
-- to minimize changes.
--
-- Also inhibit info messages that are out of scope
local function send_msg(data)
	local renamed_data = {}
	for k, v in pairs(data) do
		if k == "info" and message_level < 3 or
				k == "warning" and message_level < 2 or
				k == "error" and message_level < 1 then
			-- inhibited info message - suppress it
		else
			if rename[k] then
				-- renamed parameter - rename it
				renamed_data[rename[k]] = v
			else
				renamed_data[k] = v
			end
		end
	end
	if next(renamed_data) ~= nil then
		-- don't send empty message
		return transport:send(renamed_data)
	end
end

-- Remove any path characters from string that represents
-- a file path. Prevents path hijacking
-- eg foo/bar/../../bar/filename will return filename
local function sanitize_path(filepath)
	local clean_path
	local clean_file
	
	clean_path, clean_file = string.match(filepath,"(.-)([^/]-)$")
	return clean_file
end


-- Get the the MQTT connect params (url, userid, passwd) from config or GX's ALMS defaults
------------------------------------------------------------------
-- get_mqtt_broker - Get current MQTT broker parameters  
-- 
-- Inputs:
--
-- Outputs:
--
-- Returns:
-- 		- URL of broker (format needed my Paho MQTT containing protocol, address, and port
-- 						 as proto://address:port)
-- 		- MQTT authentication user ID
-- 		- MQTT authentication password
--
-- Notes
-- 	Takes values from telemetry.options, fills in defaults for
-- 	missing parameters.
------------------------------------------------------------------
function get_mqtt_broker()
	local address = telemetry.options.mqtt_url
	local uid = telemetry.options.mqtt_uid
	local passwd = telemetry.options.mqtt_passwd
	local cpack = telemetry.options.mqtt_certpath
	local tstore = telemetry.options.mqtt_truststore
	local kstore = telemetry.options.mqtt_keystore
	local pk = telemetry.options.mqtt_privatekey
	
	-- These settings are pre-validated as strings.
	-- no printables means use sys default
    if not string.match(address, "%S") then
       	local u = url.parse(devicetree.get("config.server.url"))
       	address = string.format("tcp://%s:%s", u.host, tostring(DEFAULT_MQTT_PORT)) 
    end
    if not string.match(uid, "%S") then
    	uid = DEVICE_ID
    end
    if not string.match(passwd, "%S") then
    	passwd = DEFAULT_PASSWORD
	end
	if not string.match(cpack, "%S") then
		cpack = ""
	else
		cpack = sanitize_path(cpack)
	end
	if not string.match(tstore, "%S") then
		tstore = ""
	else
		tstore = sanitize_path(tstore)
	end
	if not string.match(kstore, "%S") then
		kstore = ""
	else
		kstore = sanitize_path(kstore)
	end
	if not string.match(pk, "%S") then
		pk = ""
	else
		pk = sanitize_path(pk)
	end

    return address, uid, passwd, cpack, tstore, kstore, pk
end

-- airvantage handler callback to handle received
-- configuration data from AVMS
------------------------------------------------------------------
-- receive_config - AirVantage data handler callback to receive data 
-- 
-- Inputs:
-- 	assetInstance	- Asset Instance
-- 	value			- Value of received data
-- 	path			- data model path
--
-- Outputs:
-- 	telemetry.options
--
-- Returns:
-- 	"ok" to indicate to AirVantage that data has been handled
--
-- Notes
--	Receives configuration data from AirVantage system and saves to
--	configuration parameters in telemetry opbject. Updates persistent
--	object store with new data.
------------------------------------------------------------------
local function receive_config(assetInstance, value, path)
    --print("*** Received: ", assetInstance, value, path)
    local err = nil
    for pname,pvalue in pairs(value) do 

    	if type(pvalue) ~= "string" then
    		-- Sometimes we get wrong data type (function) despite specifying string. Fix it.
    		pvalue = ""
    	end
       	log(APP_NAME, "INFO", 'Configure %s="%s"', pname, tostring(pvalue))

		-- system option value are all strings
		if default_system_options[pname] then
			if not default_system_options[pname].validate(pname, pvalue) then
				-- empty or invalid value specified - use default
				log(APP_NAME, "WARNING", "Invalid value for system option %s: %s", pname, tostring(pvalue))
				telemetry.options[pname] = default_system_options[pname].value
			else
				telemetry.options[pname] = pvalue
			end
			if default_system_options[pname].handler then
				default_system_options[pname].handler(pname, telemetry.options[pname])
			end
		else
			err = BnB:set_option(telemetry.options, pname, pvalue)
			if err ~= "ok" then
				log(APP_NAME, "ERROR", "Configure error (%s) %s=%s", err, pname, tostring(pvalue))
				-- return err
			end
		end
    end

	persist.save(CONFIG_ASSET_NAME, telemetry.options)
    if restart_mqtt then
        local address, uid, passwd, cpack, tstore, kstore, pk  = get_mqtt_broker()
       	log("AVTA", "INFO", "MQTT URL updated to %s (uid=%s, passwd=%s", address, uid, passwd)
        -- restart MQTT with new URL.
		transport:stop()
		mqtt:stop()
	    mqtt:configure {
			address = address,
			userid = uid,
			password = passwd,
			certpack = cpack,
			truststore = tstore,
			keystore = kstore,
			privatekey = pk,
			}
		mqtt:run()
		transport:run()
	end
    telemetry:reconfigure()
    return "ok"
end

-- Get configuration as a table of strings, suitable for
-- sending to AMM (table of value=setting).
-- value param is list of paths to query.
local function get_config(values)
	local options = {}
	local k, v
	local pnames = {
		{ext="enable", int="en"},
		{ext="threshold", int="th"},
		{ext="min", int="mn"},
		{ext="max", int="mx"}
	}
	local simple_options = {mqtt_url=true, mqtt_uid=true, mqtt_passwd=true}
	
	if not values[1] or values[1] == "" then
		-- Empty list - request everything
		values = {}
		for param in pairs(telemetry.options) do
			table.insert(values, "downlink." .. param)
		end
	end

	for i = 1, #values do
		log("AVTA", "DEBUG", string.format("Processing (%d) %s", i, values[i]))
		local pos, n = string.find(values[i], "downlink.")
		if pos == 1 then	-- path gotta start at 1st char
			k = string.sub(values[i], n + 1)
			v = telemetry.options[k]
		else
			k, v = nil, nil
		end

		if v then
			if default_system_options[k] then
				-- if param is in system defaults then it's a (simple) system setting
				options[k] = tostring(v)
			elseif simple_options[k] then
				-- Simple value - report it
				options[k] = telemetry.options[k]
			else
				-- it's a compound telemetry setting (enable, threshold, min, max)
				local PIDInfo = telemetry.streamer.PIDInfoByName[k]
				if PIDInfo then	-- make sure it's a valid option
					local opts = ""
					for i = 1, #pnames do
						if not PIDInfo.mask or not PIDInfo.mask[pnames[i].int] then
							-- hide masked sub-params according to master table
							opt = pnames[i].ext .. '=' .. v[pnames[i].int]
							if opts == "" then
								opts = opt
							else
								opts = opts .. "," .. opt
							end
						end
					end
					options[k] = opts
				end
			end
		elseif simple_options[k] then
			-- Empty simple value - report empty string
			options[k] = ""
		end
	end
	return options
end

-- airvantage handler ReadNode callback to send configuration to ALMS.
-- Path should always be "ReadNode", value should be a list of
-- parameter names to read (these should be prefixed with "downlink.",
-- since that the asset used for config.
-- Note: Unlike native ReadNode, this does not handle recursion.
local function request_config(assetInstance, value, path)
    -- p("*** Requested: ", assetInstance, value, path)
	local options = get_config(value)
	assert(assetInstance:pushData("downlink", options, "now"))
	return "ok"
end

-- Same as above, but the request is from AMM
local function amm_request_config(assetInstance, value, path)
    -- p("*** Requested (AMM): ", assetInstance, value, path)
	local options = get_config("")
	assert(assetInstance:pushData("uplink", options, "now"))
	return "ok"
end

-- Split a (comma) separated string into an array of fields
local function split(inputstr, sep)
	if sep == nil then
		sep = ","
	end
	local t={} ; i=1
	for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
		t[i] = str
		i = i + 1
	end
	return t
end

-- Add gps info (if fix available) to the event.
local function add_gps_data(event)
	-- Get GPS location and heading
	local gpsdata = devicetree.get({"system.gps.fix", "system.gps.latitude", "system.gps.longitude", "system.gps.heading"})
	if gpsdata and gpsdata['system.gps.fix'] ~= 0 and (gpsdata['system.gps.latitude'] ~= 0 or gpsdata['system.gps.longitude'] ~= 0) then
		event.gps_latitude = gpsdata['system.gps.latitude']
		event.gps_longitude = gpsdata['system.gps.longitude']
		event.gps_heading = gpsdata['system.gps.heading']
	end
end

-- Add cell info to a WAN event
local function add_cell_info(event)
	-- Get cell info stuff
	local celldata = devicetree.get({msciid.STS_CELL_ROAM_OPERATOR, msciid.STS_CELL_RAT, msciid.STS_CELL_BAND})
	event.carrier = celldata[ msciid.STS_CELL_ROAM_OPERATOR ]
	event.cellrat = celldata[ msciid.STS_CELL_RAT ]
	event.celllteband = celldata[ msciid.STS_CELL_BAND ]
end

-- Add wifi info to a WAN event
local function add_wifi_info(event)
	-- Get wifi info stuff
	local wifidata = devicetree.get({msciid.STS_WIFI_MODE_A,msciid.STS_WIFI_MODE_B,msciid.STS_WIFI_A_AVAILABLE_AP,msciid.STS_WIFI_B_AVAILABLE_AP})
	if wifidata[ msciid.STS_WIFI_MODE_A ] == 2
	then 
		event.wifiwan = "WiFi_A"
		event.wifissid = wifidata[ msciid.STS_WIFI_A_AVAILABLE_AP ]
	elseif wifidata[ msciid.STS_WIFI_MODE_B ] == 2
	then
		event.wifiwan = "WiFi_B"
		event.wifissid = wifidata[ msciid.STS_WIFI_B_AVAILABLE_AP ]
	else
		-- neither wifi radio is in client mode, weird
	end
end

------------------------------------------------------------------
-- getincid - get the next incident id
-- 
-- Inputs:
--
-- Outputs:
--
-- Returns:
--	unique incident id (string) that looks like an int
--	least 8 digs count, Sig digs are boot count)
--
-- Notes
--	Call as incid:get()
--
--	Generate and return an incident ID that is virtually guaranteed
--	to be unique across the lifetime of the gateway. Combination of
--	boot count and random initial value should ensure uniqueness under
--	most conditions (at least 1/10000 chance of collision if app
--	re-started without rebooting)
--
------------------------------------------------------------------
local incid = {
	boots = -1,
	count = false,

	-- member func to alloctate an iid
	get = function (self)
		-- No inc id yet - assign a random one
		if not self.count then
			-- seed count with 8 digits of fractional part of uptime (nS), reversed
			-- Since this is sampled at a time determined solely by an external (incident) event,
			-- this should be about as completely random as possible.
			self.count = tonumber(string.format("%.8f", uptime.time()):reverse():sub(1,8))
			self.boots = tonumber(devicetree.get(msciid.STS_SYSTEM_BOOTS))
		else
			self.count = self.count + 1
		end
		return string.format("%d%08d", self.boots, self.count)
	end
}

-- Collect data for event t and generate an event from it. Some events
-- also apply the telemetry config filter to suppress unwanted events.
-- Some events also use v parameter, which contains the devicetree data
-- that triggered the event. Not all events use this (though more probably
-- should). Note that calling devicetree.get() can cause AAF to yield,
-- possibly causing unexpected results.
local function getevent(t, v)
    local event = {}

	if t == "startup" then
		-- Filter notification (enable only)
		local tmp = {["gateway_start_stop"] = 0}
		telemetry:filter(tmp)
		if tmp["gateway_start_stop"] then
			event[t] = 0				-- dummy value
			event.timestamp = 0			-- flag to timestamp as initial boot time
		else
			event = nil
		end

	elseif t == "appinfo" then
		event.info = APP_NAME .." Ver = " .. pkginfo.manifest.components[1].version ..
				      ", build = " .. pkginfo.build ..
				      ", commit = " .. pkginfo.commit ..
					  ", ATP = " .. ATP_VERSION
		log(APP_NAME, "INFO", event.info)

	elseif t == "gwinfo" then
		local tmp = { [t] = 0 } -- no threshold to filter on
		telemetry:filter(tmp)
		if tmp[t] == nil then
			event = nil
		else
			-- Send gwinfo parameters
			event.gwimei = tostring(devicetree.get("system.cellular..hw_info.imei"))
		end

	elseif t == "ignitionstate" or t == "bootignitionstate" then
		if t == "bootignitionstate" then
			event.timestamp = 0		-- flag to timestamp as initial boot time
		end

		local ignstate

		if IGNITION_SENSE_LINE == "always_on" then
			ignstate = 1
		else
			ignstate = devicetree.get(IGNITION_SENSE_LINE)
		end
		event.ignition_status = ignstate
		
		local tmp = {["ignition_status"] = ignstate}
		telemetry:filter(tmp)
		if tmp["ignition_status"] == nil then
			event = nil
		else
			add_gps_data(event)
		end

	elseif t == "heartbeat" then
		local tmp = { [t] = 0 } -- no threshold to filter on
		telemetry:filter(tmp)
		if tmp[t] == nil then
			event = nil
		else
			persist.save("lastheartbeat", transport:getValidtime() )
			event[t] = math.floor(uptime.time())
		end

	elseif t == "uncleanshutdown" then
		-- Filter notification (enable only)
		local tmp = {["gateway_start_stop"] = 0}
		telemetry:filter(tmp)
		if tmp["gateway_start_stop"] then
			local lasthb = persist.load("lastheartbeat")
			event.shutdown = 1
			if type(lasthb) == "number" then
				event.timestamp = lasthb
			else
				log(APP_NAME, "WARNING", "Unclean shutdown event suppressed - invalid timestamp" )
			end
		else
			event = nil
		end

	elseif t == "shutdown" then
		-- shutdown info persisted on last shutdown, because there's no direct way to discern
		-- app shutdown from OS shutdown. So, we catch the last one before actual reboot
		-- (want to show os shutdown, not app)
		-- Filter notification (enable only)
		local tmp = {["gateway_start_stop"] = 0}
		telemetry:filter(tmp)
		if tmp["gateway_start_stop"] then
			local lastsd = persist.load("lastshutdown")
			if lastsd then
				event.shutdown = 0
				if lastsd.time then
					-- use shutdown timestamp if valid
					event.timestamp = lastsd.time
				end
			else
				-- no shutdown info - no event
				event = nil
			end
		else
			event = nil
		end

	elseif t == "gpsfix" then
	    event.gpsstate = devicetree.get('system.gps.fix')
	    event.gpssatnum = devicetree.get('system.gps.sat_cnt')
	    event.gpsquality = devicetree.get('system.gps.qual')

		if event.gpsstate and event.gpssatnum and event.gpsquality then
			-- Filter 3 values as string, so we send if anything changes
			local tmp = { [t] = string.format("%d,%d,%d", event.gpsstate, event.gpssatnum, event.gpsquality)}
			telemetry:filter(tmp)
			if tmp[t] == nil then
				event = nil
			end
		else
			-- no GPS available
			event = nil
		end

-- The remaining parameters are filtered via the telemetry filtering mechanism
-- so that the reporting frequency may be configured. The data for these is
-- generally polled rathar than triggered on change.

	elseif t == "wanlinktxbytes" then
		local tevt = wanstate.getCellTxEvent()
		if tevt then
			event[t] = tevt.param1
			event.linkinfo = DPItoIndex[tevt.param0]
			local tmp = { [t] = tevt.param1 }
			telemetry:filter(tmp)
			if tmp[t] == nil then
				event = nil
			end
		else
			event = nil
		end

	elseif t == "wanlinkrxbytes" then
		local tevt = wanstate.getCellRxEvent()
		if tevt then
			event[t] = tevt.param1
			event.linkinfo = DPItoIndex[tevt.param0]
			local tmp = { [t] = tevt.param1 }
			telemetry:filter(tmp)
			if tmp[t] == nil then
				event = nil
			end
		else
			event = nil
		end

	elseif t == "gpslocation" then
		-- Read GPS coords, convert to 9.23 format
		local gpsdata = devicetree.get({"system.gps.latitude", "system.gps.longitude"})
		if gpsdata then
			event.gps_latitude = gpsdata['system.gps.latitude']
			event.gps_longitude = gpsdata['system.gps.longitude']

			-- Run thru filter to reduce data usage
			local tmp = { [t] = {latitude = gpsdata['system.gps.latitude'], longitude = gpsdata['system.gps.longitude']} }
			telemetry:filter(tmp)
			if tmp[t] == nil or (gpsdata['system.gps.latitude'] == 0 and gpsdata['system.gps.longitude'] == 0) then
				event = nil
			end
		else
			-- no GPS available
			event = nil
		end

	elseif t == "gpsspeed" then
		-- Convert to M/H, filter via telemetry filter
		local gpsdata = devicetree.get({"system.gps.fix", "system.gps.speed", "system.gps.latitude", "system.gps.longitude", "system.gps.heading"})
		if gpsdata then
			event[t] = gpsdata['system.gps.speed']
			event.gps_latitude = gpsdata['system.gps.latitude']
			event.gps_longitude = gpsdata['system.gps.longitude']
			event.gps_heading = gpsdata['system.gps.heading']
			
			local tmp = { [t] = event[t] }
			telemetry:filter(tmp)
			if tmp[t] == nil or gpsdata['system.gps.fix'] == 0 or (gpsdata['system.gps.latitude'] == 0 and gpsdata['system.gps.longitude'] == 0) then
				event = nil
			end
		else
			-- no GPS available
			event = nil
		end

	-- set of accelerometer start events
	elseif vbistartparms[t] then
		-- Run thru telmetry filter
		-- Note that ext threshold (always triggers) is used, and
		-- min rate will never be respected, because this is not polled.
		local tmp = { [t] = 0 }
		telemetry:filter(tmp)
		if tmp[t] == nil then
			event = nil
		else
			event["evt_" .. t] = 1	-- create event name param

			local iname = vbistartparms[t].name

			-- Split the event data string into individual parameters
			-- Use the data from the event - presumably this is queued so
			-- we won't miss events.
			local params = split(v)	-- [1]=1st reading past threshold, [2]=event counter

			-- report threshold setting at event start
			event[iname .. "_threshold"] = tonumber(devicetree.get(vbithresh[iname]))
			event[iname .. "_initial"] = tonumber(params[1])		-- 1st reading after threshold crossed

			-- Get GPS location and heading
			add_gps_data(event)

			-- Assign an incident identifier to this incident
			if active_incident_ids[iname] then
				log(APP_NAME, "WARNING", APP_NAME .. " " .. t .. " received while still active")
			end
			active_incident_ids[iname] = incid:get()
			event.incident_id = active_incident_ids[iname]
		end
	-- set of accelerometer end events
	elseif vbiendparms[t] then
		-- Run thru telmetry filter
		-- Note that ext threshold (always triggers) is used, and
		-- min rate will never be respected, because this is not polled.
		local tmp = { [t] = 0 }
		telemetry:filter(tmp)
		if tmp[t] == nil then
			event = nil
		else
			event["evt_" .. t] = 1	-- create event name param

			local iname = vbiendparms[t].name

			-- Split the event data string into individual parameters
			-- Use the data from the event - presumably this is queued so
			-- we won't miss events.
			local params = split(v)	-- [1]=duration, [2]=peak accel, [3]=average, [4]=event counter
			event[iname .. "_duration"] = tonumber(params[1])		-- event duration
			event[iname .. "_peak"] = tonumber(params[2])		-- peak acceleration
			event[iname .. "_average"] = tonumber(params[3])		-- average acceleration
			
			-- Get GPS location and heading
			add_gps_data(event)

			-- Get incident identifier from previous start
			if not active_incident_ids[iname] then
				log(APP_NAME, "WARNING", APP_NAME .. " " .. t .. " received without corresponding start")
				-- End event without start is mostly useless, so throw it away. This means we miss a driver
				-- behaviour event entirely, but it seems better than confusing the issue with half an event
				-- TBD: An alternative here is to construct a start event and send it too. But we'd have
				-- no knowledge of the initial value and GPS co-ords, so it might be misleading.
				event = nil
			else
				event.incident_id = active_incident_ids[iname]
				active_incident_ids[iname] = nil
			end
		end
	elseif t == "dreckon_sensor_calibration" then
		event["evt_" .. t] = 1	-- create event name param
		event[t] = tonumber(devicetree.get(msciid.STS_DRECKON_SENSOR_CALIBRATED))
	
		-- Get GPS location and heading
		add_gps_data(event)

		local tmp = { [t] = 0 }
		telemetry:filter(tmp)
		if tmp[t] == nil or devicetree.get(msciid.MP70_VEH_BEHAVE_EN) ~= "1" then
			event = nil
		end
	elseif t == "gpioinput" then
		local io = 0
		for k, v in pairs(gpiobits) do
			if devicetree.get(k) == 1 then
				io = io + v
			end
		end
		event[t] = io
		telemetry:filter(event)
		
		if event[t] then
			-- Get GPS location and heading
			add_gps_data(event)
		else
			event = nil
		end
	elseif t == "cell_rssi" then
		-- Convert to M/H, filter via telemetry filter
		local celldata = devicetree.get( "system.cellular.link.rssi" )
		if celldata then
			event[t] = tonumber(celldata)

			telemetry:filter(event)
			if event[t] then
				-- Get GPS location and heading
				add_gps_data(event)		
				log( APP_NAME, "DEBUG", "Sending rssi, value %d", event[t] )
			else
				event = nil
			end
		else
			-- no Cell stats available
			event = nil
		end
	elseif t == "lte_rsrp" then
		-- Convert to M/H, filter via telemetry filter
		local celldata = devicetree.get("system.cellular.lte.link.rsrp")
		if celldata then
			event[t] = tonumber(celldata)
			
			telemetry:filter(event)
			if event[t] then
				-- Get GPS location and heading
				add_gps_data(event)		
				log( APP_NAME, "DEBUG", "Sending rsrp, value %d", event[t] )
			else
				event = nil
			end
		else
			-- no Cell stats available
			event = nil
		end
	elseif t == "lte_rsrq" then
		-- Convert to M/H, filter via telemetry filter
		local celldata = devicetree.get("system.cellular.lte.link.rsrq")
		if celldata then
			event[t] = tonumber(celldata)
			
			telemetry:filter(event)
			if event[t] then
				-- Get GPS location and heading
				add_gps_data(event)		
				log( APP_NAME, "DEBUG", "Sending rsrp, value %d", event[t] )
			else
				event = nil
			end
		else
			-- no Cell stats available
			event = nil
		end
	elseif t == "lte_sinr" then
		-- Convert to M/H, filter via telemetry filter
		local celldata = devicetree.get( msciid.STS_LTE_SINR )
		if celldata then
			event[t] = tonumber(celldata)
			
			telemetry:filter(event)
			if event[t] then
				-- Get GPS location and heading
				add_gps_data(event)		
				log( APP_NAME, "DEBUG", "Sending sinr, value %d", event[t] )
			else
				event = nil
			end
		else
			-- no Cell stats available
			event = nil
		end
	elseif t == "carrier" then
		local celldata = devicetree.get( msciid.STS_CELL_ROAM_OPERATOR )
		if celldata then
			event[t] = celldata
			telemetry:filter(event)
			if event[t] then
				-- Get GPS location and heading
				add_gps_data(event)		
				log( APP_NAME, "DEBUG", "Sending cellular name, value %s", event[t] )
			else
				event = nil
			end
		else
			-- no Cellular info available
			event = nil
		end
	elseif t == "cellrat" then
		local celldata = devicetree.get( msciid.STS_CELL_RAT )
		if celldata then
			event[t] = celldata
			telemetry:filter(event)
			if event[t] then
				-- Get GPS location and heading
				add_gps_data(event)		
				log( APP_NAME, "DEBUG", "Sending cellular RAT, value %s", event[t] )
			else
				event = nil
			end
		else
			-- no Cellular info available
			event = nil
		end
	elseif t == "celllteband"
	 then
		local celldata = devicetree.get( msciid.STS_CELL_BAND )
		if celldata then
			event[t] = celldata
			telemetry:filter(event)
			if event[t] then
				-- Get GPS location and heading
				add_gps_data(event)		
				log( APP_NAME, "DEBUG", "Sending cellular LTE band, value %s", event[t] )
			else
				event = nil
			end
		else
			-- no Cellular info available
			event = nil
		end

	else
		--unhandled event
		event = nil
	end

	return event
end

local function generateevent(t, v)
    local content = getevent(t, v)
	if content then
		send_msg(content)
	end
end

local function sendProprietaryTelemetryData()

    if BnB.get_CustomTelemData_enable() == 1 then
        if next(TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE) ~= nil then
            local now = os.time()
            if now >= (TELEM_CFG_FILE_SIGNAL_LAST_PUBLISH + BnB.get_CustomTelemData_max()) then

                -- Can send now without violating reporting throttle period
                log(APP_NAME, "DEBUG", "AVTA-105 - sendProprietaryTelemetryData() calling send_msg()")
                send_msg(TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE)

                TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE = { }
                TELEM_CFG_FILE_SIGNAL_LAST_PUBLISH  = now
            end
        end
    end
end

local function throttleProprietaryTelemetryReporting()

    while true do
        if next(TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE) ~= nil then
            sendProprietaryTelemetryData()
        end
        sched.wait(1)
    end
end

local function handleProprietaryTelemetryMsciid(vars)

    for k,v in pairs(vars) do
        log(APP_NAME, "DEBUG", "handleProprietaryTelemetryMsciid(): "..k.."="..v)

        local name = TELEM_CFG_FILE_SIGNAL_NAME_TBL[k]
        if name then
            TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE[name] = v
            --log(APP_NAME, "DEBUG", "AVTA-105 - handleProprietaryTelemetryMsciid(): create event for... "..name)
        end
    end

    if next(TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE) ~= nil then
        sendProprietaryTelemetryData()
    end
end

local function handleCfgFileDiscovery()

    if (TELEM_CFG_FILE_TITLE and TELEM_CFG_FILE_REVISION) then
        --log(APP_NAME, "DEBUG", "AVTA-105 - TITLE/REVISION = "..TELEM_CFG_FILE_TITLE.."/"..TELEM_CFG_FILE_REVISION)

        -- If new file set has been installed, cleanup any leftover stuff
        TELEM_CFG_FILE_SIGNAL_NAME_TBL = { }
        TELEM_CFG_FILE_SIGNAL_UNIT_TBL = { }
        for k,v in pairs(TELEM_CFG_FILE_SIGNAL_REGISTERED_USERID_TBL) do

            devicetree.unregister(v)
            sched.wait(0.2)
        end
        TELEM_CFG_FILE_SIGNAL_REGISTERED_USERID_TBL = { }

        -- Build up list of candidate files to check
        local fileList = { }
        local numFiles = 0
        local attr = nil

        -- Is this gateway running 4.13.0+ ALEOS code?
        local activeTelemetryCfgFile = "/tmp/activeTelemetryCfg.txt" -- ALEOS Release 4.13.0+ CanMgr will dump a copy here for AMMER|AVTA to retrieve
        attr = lfs.attributes(activeTelemetryCfgFile)
        if attr == nil then
            -- Nope.
            --log(APP_NAME, "DEBUG", "AVTA-105 - "..activeTelemetryCfgFile.." does not exist")
        else
            -- Yep.
            numFiles = numFiles + 1
            fileList[numFiles] = activeTelemetryCfgFile
        end

        -- Was telemetry configuration file installed via the AVTC application?
        local avtcDir = "/mnt/user/readyagent/apps/AVTC/vehiclecfg/"
        attr = nil
        attr = lfs.attributes(avtcDir)
        if attr == nil then
            -- Nope.
            --log(APP_NAME, "DEBUG", "AVTA-105 - "..avtcDir.." does not exist")
        else
            if attr.mode == "directory" then
                -- Yep.
                for file in lfs.dir(avtcDir) do
                    if file ~= "." and file ~= ".." then
                        attr = lfs.attributes(avtcDir..file)
                        if attr.mode ~= "directory" then
                            numFiles  = numFiles + 1
                            fileList[numFiles] = avtcDir..file
                        end
                    end
                end
            else
                -- How can this be???
                log(APP_NAME, "ERROR", avtcDir.." is not a directory!")
            end
        end

        -- Go through candidate list until file with matching title/revision found
        if numFiles < 0 then
            log(APP_NAME, "ERROR", "CAN Mgr advertises Title/Revision but no Telemetry Cfg Files exist!")
            return
        end

        local noMatchFound = true
        local tbl = nil

        for i = 1, numFiles do

            -- Read whole file into a string
            local f = io.open(fileList[i], "rb")
            local content = f:read("*all")
            f:close()

            -- Check file for matching title/revision
            tbl = json.decode(content)
            --log(APP_NAME, "DEBUG", "AVTA-105 - checking file "..fileList[i].." - TITLE/REVISION = "..tbl.VTConfig.title.."/"..tbl.VTConfig.revision)

            if (TELEM_CFG_FILE_TITLE    == tbl.VTConfig.title and
                TELEM_CFG_FILE_REVISION == tbl.VTConfig.revision) then

                noMatchFound = false
                break -- BREAK out of FOR loop; title+revision matched!
            end
        end

        if noMatchFound then

            log(APP_NAME, "ERROR", "No match found for telemetry cfg file title/revision = "..TELEM_CFG_FILE_TITLE.."/"..TELEM_CFG_FILE_REVISION)
            return
        end

        -- Go through contents of 'tbl'
        for i = 1, #tbl.VTConfig.messages do

            for j = 1, #tbl.VTConfig.messages[i].signals do

                -- Proprietary MSCIID range = 59101 to 59150
                if tbl.VTConfig.messages[i].signals[j].itemId >= 59101 and
                   tbl.VTConfig.messages[i].signals[j].itemId <= 59150 then

                    if tbl.VTConfig.messages[i].signals[j].name then

                        local msciid = "aleos."..tbl.VTConfig.messages[i].signals[j].itemId
                        local signalName = string.gsub(tbl.VTConfig.messages[i].signals[j].name, " ", "_")
                        -- Replaced <space> characters in signal name with <underscore> character.

                        -- Register newly discovered "proprietary" MSCIIDs with CSM
                        TELEM_CFG_FILE_SIGNAL_REGISTERED_USERID_TBL[msciid] = devicetree.register(msciid, handleProprietaryTelemetryMsciid)
                        -- Read intial value
                        local initialValue = devicetree.get(msciid)
                        if initialValue ~= "" then
                            -- This MSCIID has indeed been written to before.
                            TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE[signalName] = initialValue
                        end
                        sched.wait(0.2)

                        -- Track and report these signals (proprietary MSCIIDs fall outside BBSE supported items)
                        TELEM_CFG_FILE_SIGNAL_NAME_TBL[msciid] = signalName

                        if tbl.VTConfig.messages[i].signals[j].units then
                            TELEM_CFG_FILE_SIGNAL_UNIT_TBL[msciid] = tbl.VTConfig.messages[i].signals[j].units
                        end
                    end
                end

                --[[ log(APP_NAME, "DEBUG", "AVTA-105 -- msciId = "..tbl.VTConfig.messages[i].signals[j].itemId)
                if tbl.VTConfig.messages[i].signals[j].units then
                    log(APP_NAME, "DEBUG", "AVTA-105 --- units = "..tbl.VTConfig.messages[i].signals[j].units)
                end
                if tbl.VTConfig.messages[i].signals[j].name then
                    log(APP_NAME, "DEBUG", "AVTA-105 ---- name = "..tbl.VTConfig.messages[i].signals[j].name)
                end ]]
            end
        end

        if next(TELEM_CFG_FILE_SIGNAL_SAMPLES_CACHE) ~= nil then
            sendProprietaryTelemetryData()
        end

        --[[ log(APP_NAME, "DEBUG", "AVTA-105 --- TITLE/REVISION MATCH FOUND")
        for k, v in pairs(TELEM_CFG_FILE_SIGNAL_NAME_TBL) do
            log(APP_NAME, "DEBUG", "AVTA-105 ----- Proprietary MSCIID = "..k)
            log(APP_NAME, "DEBUG", "AVTA-105 ------------------- name = "..v)
            if TELEM_CFG_FILE_SIGNAL_UNIT_TBL[k] then
                log(APP_NAME, "DEBUG", "AVTA-105 ------------------- unit = "..TELEM_CFG_FILE_SIGNAL_UNIT_TBL[k])
            end
        end ]]
    end
end

local function msciidtoevent(vars)
	for k,v in pairs(vars) do
		log(APP_NAME, "DEBUG", "msciidtoevent: "..k.."="..v)
	end
	if vars[msciid.STS_TIME_VALID] then
		transport:setTimeValid()
		transport:send(nil)
	end
    if vars["system.gps.fix"] or
		vars["system.gps.qual"] or
	   (vars["system.gps.sat_cnt"] and vars["system.gps.sat_cnt"] <= 4) then
    	generateevent("gpsfix")
    end
    if vars["system.gps.speed"] then
    	generateevent("gpsspeed")
    end
	if vars[msciid.STS_DRECKON_SENSOR_CALIBRATED] then
		generateevent("dreckon_sensor_calibration")
	end

	 if vars[msciid.STS_CELL_ROAM_OPERATOR] then
		log(APP_NAME,"DEBUG", "cell name notification")
		generateevent("carrier")
	end

	if vars[msciid.STS_CELL_RAT] then
		log(APP_NAME,"DEBUG", "cell rat notification")
		generateevent("cellrat")
	end
		
	if vars[ msciid.STS_CELL_BAND ] then
		log(APP_NAME,"DEBUG", "cell band notification")
		generateevent("celllteband")
	end
		
	-- check list of vehicle behaviour events
	for evt,dtname in pairs(vbistartparms) do
		if vars[dtname.csm] then
			generateevent(evt, vars[dtname.csm])
		end
	end
	for evt,dtname in pairs(vbiendparms) do
		if vars[dtname.csm] then
			generateevent(evt, vars[dtname.csm])
		end
	end

	for evt in pairs(gpiobits) do
		if vars[evt] then
			generateevent("gpioinput")
			break
		end
	end

    if vars[msciid.STS_TELEM_CFG_FILE_TITLE] then
        TELEM_CFG_FILE_TITLE = vars[msciid.STS_TELEM_CFG_FILE_TITLE]
        --log(APP_NAME, "DEBUG", "AVTA-105 - MSCIID_STS_TELEM_CFG_FILE_TITLE just set to "..TELEM_CFG_FILE_TITLE)
        handleCfgFileDiscovery()
    end

    if vars[msciid.STS_TELEM_CFG_FILE_REVISION] then
        TELEM_CFG_FILE_REVISION = vars[msciid.STS_TELEM_CFG_FILE_REVISION]
        --log(APP_NAME, "DEBUG", "AVTA-105 - MSCIID_STS_TELEM_CFG_FILE_REVISION just set to "..TELEM_CFG_FILE_REVISION)
        handleCfgFileDiscovery()
    end

    if BnB.get_CustomTelemData_enable() == 1 then
        if vars[msciid.STS_TELEM_SCAN_TOOL_STATUS] then
            TELEM_SCAN_TOOL_STATUS = vars[msciid.STS_TELEM_SCAN_TOOL_STATUS]
            local event = {}
            event["scantool_status"] = TELEM_SCAN_TOOL_STATUS
            send_msg(event)
        end
    end

    return "ok"
end

-- Send an update of WAN conditions to AMM. This is called
-- when a change in network status is detected. WAN link state
-- generation always looks at entire WAN state, not just changes.
-- Note that createWanLinkEvents() was originally written for AMMER,
-- and generates DELS format events. This code uses that, unchanged,
-- and extract the information from it's output to create events
-- formatted for AVTA (MQTT).
local WanUpdateFilter = {}
function updateWanEvents()
	local links = wanstate.extractWanLinks()
	local events = wanstate.createWanLinkEvents(links)
	local evts = events
	local waninfo = {vpnstatus=0, wanstatus=0, activewan=DPItoIndex[0]}
	local action = {
		-- Table of functions to call, one for each event type
		-- Used to process the DELS (AMMER) formatted events from createWanLinks
		-- into AVTA's format
		[wanstate.EventModel.DELS_WANSTATUS] = function(p0, p1)
			waninfo.wanstatus = p0
		end,
		[wanstate.EventModel.DELS_VPNSTATUS] = function(p0, p1)
			waninfo.vpnstatus = p0
		end,
		[wanstate.EventModel.DELS_WANUP] = function(p0, p1)
			if not waninfo[DPItoIndex[p0]] then waninfo[DPItoIndex[p0]] = {} end
			waninfo[DPItoIndex[p0]].state = 1
		end,
		[wanstate.EventModel.DELS_WANDOWN] = function(p0, p1)
			if not waninfo[DPItoIndex[p0]] then waninfo[DPItoIndex[p0]] = {} end
			waninfo[DPItoIndex[p0]].state = 0
			waninfo[DPItoIndex[p0]].address = 0
		end,
		[wanstate.EventModel.DELS_WANACTIVE] = function(p0, p1)
			waninfo.activewan = DPItoIndex[p0]
		end,
		[wanstate.EventModel.DELS_WANNOTACTIVE] = function(p0, p1)
		end,
		[wanstate.EventModel.DELS_WANADDRESS] = function(p0, p1)
			if not waninfo[DPItoIndex[p0]] then waninfo[DPItoIndex[p0]] = {} end
			waninfo[DPItoIndex[p0]].address = p1
		end,
	}
	
	-- Process the list of events generated into AVTA format
	for i, evt in ipairs(events) do
--		p(evt.messageid, action[evt.messageid])
		if type(action[evt.messageid]) == "function" then
			action[evt.messageid](evt.param0, evt.param1)
		end
	end
	
	-- Send only the ones that have changed since last time
	if waninfo.activewan ~= WanUpdateFilter.activewan then
		-- Filter notification (enable only)
		local tmp = { ["primarywan"] = DPItoIndex[0] }
		log(APP_NAME,"DEBUG","wan change - %s",waninfo.activewan )			
		telemetry:filter(tmp)
		if tmp["primarywan"] then
			local e = {primarywan=waninfo.activewan}
			add_gps_data(e)
			log(APP_NAME,"DEBUG","wan change - %s",waninfo.activewan )			

			if waninfo.activewan == "cell1" then
				add_cell_info(e)
			elseif waninfo.activewan == "wifi1" then
				log(APP_NAME,"DEBUG","adding wifi info" )			
				add_wifi_info(e)
			end
			send_msg (e)
		end
	end
	if waninfo.vpnstatus ~= WanUpdateFilter.vpnstatus then
		-- Filter notification (enable only)
		local tmp = { ["vpnstatus"] = 0 }
		telemetry:filter(tmp)
		if tmp["vpnstatus"] then
			local e = {vpnstatus=waninfo.vpnstatus}
			add_gps_data(e)
			send_msg (e)
		end
	end
	for dpi, index in pairs(DPItoIndex) do
		if waninfo[index] then
			if not WanUpdateFilter[index] or
					WanUpdateFilter[index].state ~= waninfo[index].state or
					WanUpdateFilter[index].address ~= waninfo[index].address then
				-- Filter notification (enable only)
				local tmp = { ["wanstatus"] = 0 }
				telemetry:filter(tmp)
				if tmp["wanstatus"] then
					local e = {wanstatus=waninfo[index].state, wanipaddress=waninfo[index].address, linkinfo=index}
					add_gps_data(e)
					send_msg (e)
				end
			end
		end
	end
	WanUpdateFilter = waninfo
end

-- Handle WAN state notification from ALEOS.
-- The action runs after a delay, to prevent multiple notifications from
-- creating a flurry of DELS events (allow time to settle down first).
local WanUpdateTimer
local function dlyUpdateWanEvents(evts)
	if not WanUpdateTimer then
		WanUpdateTimer = timer.once(3,
		function() 
		-- Do work here
		updateWanEvents()
		end )
	else
		timer.rearm(WanUpdateTimer)
	end
end

-- Register for change notifications in response to changing conditions
local function registertoaleos()
	local registerIds = {}	-- table of registration IDs
	local parms = {
		msciid.STS_TIME_VALID,
		msciid.STS_DRECKON_SENSOR_CALIBRATED,
        msciid.STS_TELEM_CFG_FILE_TITLE,
        msciid.STS_TELEM_CFG_FILE_REVISION,
		"system.gps.fix",
		"system.gps.sat_cnt",
		msciid.STS_CELL_ROAM_OPERATOR,
		msciid.STS_CELL_RAT,
		msciid.STS_CELL_BAND,
		msciid.STS_TELEM_SCAN_TOOL_STATUS
	}

	for k,v in pairs(vbistartparms) do
		table.insert(parms, v.csm)
	end
	for k,v in pairs(vbiendparms) do
		table.insert(parms, v.csm)
	end
	for k in pairs(gpiobits) do
		table.insert(parms, k)
	end

	-- Due to a bug in ALEOS, registering all of the required notifications overflows
	-- the CSM queue. The work-around here slows down the registration process so that
	-- this does not occur.
--	devicetree.register(parms, msciidtoevent)
	for i=1, #parms do table.insert(registerIds, devicetree.register(parms[i], msciidtoevent)) sched.wait(0.2) end

--	devicetree.register(wanstate.msciids, dlyUpdateWanEvents)
	for i=1, #wanstate.msciids do table.insert(registerIds, devicetree.register(wanstate.msciids[i], dlyUpdateWanEvents)) sched.wait(0.2) end

	return registerIds
end

local function checkstartupdown()
	-- Only send boot event one per boot (in case app re-starts).
	-- If flag file (in ramdisk, gets cleared at boot) is present,
	-- startup has already been processed (app is restarting), so bypass.
	-- If flag file not present this is first time since boot, so create it and process.
	local f = io.open(bootflagfile, "r")
	if not f then
		f = io.open(bootflagfile, "w")
		io.close(f)

		local uncleanshutdown = persist.load("lastheartbeat")

		if uncleanshutdown then
			generateevent("uncleanshutdown")
		else
			generateevent("shutdown")
		end
		persist.save("lastshutdown", nil)

		generateevent("startup")
		generateevent("bootignitionstate")
		generateevent("gpslocation")
		generateevent("dreckon_sensor_calibration")
		-- Reset BB streamer trip data once on boot 
		telemetry:trip_reset()

	else
		io.close(f)
	end

    generateevent("appinfo")
	generateevent("gwinfo")
end

-- Format and send telemetry data
-- Splits each telemetry datapoint into a separate event
local function format_mqtt_msg(data)
    for name, val in pairs(data) do
		local msg = {[name] = val}
		if name == "engine_run_time" then
			add_gps_data(msg)
		end
		send_msg( msg )
    end
end
-- ----------------------------------------------------------------------------
-- PROCESSES
-- ----------------------------------------------------------------------------
------------------------------------------------------------------
-- main - Main App Process 
-- 
-- Inputs:
--
-- Outputs:
--
-- Returns:
--
-- Notes
------------------------------------------------------------------
local function main()
--	log.displaylogger = function(mod, sev, msg) print(mod, sev, msg) end
    assert(devicetree.init())
    assert(airvantage.init())
	
	local serial_port

	log(APP_NAME, "INFO", APP_NAME.." Started")
	--print("Executing from: "..os.getenv("PWD"))

	DEVICE_ID = devicetree.get("system.hw_info.global_id")
	SW_VERSION = devicetree.get("system.sw_info.fw_ver")
	MQTT_DATA_PATH = DEVICE_ID.."/messages/json"

	-- Determine which line to monitor for ignition input
	-- If MP70 telemetry enabled and using I/O for ignition
	-- then monitor I/O, else monitor ignition.
	-- (If telemetry is disabled, use ignition, because that's the preferred wiring)
	-- For non MP70 the MSCIIDs will just be 0.
	IGNITION_SENSE_LINE = "system.io.ignition_in"
	IGNITION_SENSE_TIME = msciid.CFG_LOW_PWR_IGNITION_TIMER

	-- This should work for at least the new few versions of ALEOS. After that, everything will be different
	if string.match( SW_VERSION, "^4.1[1-9]" )  then
		local can_cfg = devicetree.get( msciid.CANBUS_ENABLED_IGN_SOURCE )
		if can_cfg == "0" then
			IGNITION_SENSE_LINE = "system.io.ignition_in"
			IGNITION_SENSE_TIME = msciid.CFG_LOW_PWR_IGNITION_TIMER
		elseif can_cfg == "1" then
			IGNITION_SENSE_LINE = "system.io.ignition_in"
			IGNITION_SENSE_TIME = msciid.CFG_LOW_PWR_IGNITION_TIMER
		elseif can_cfg == "2" then
			IGNITION_SENSE_LINE = "system.io.gpio.1"
			IGNITION_SENSE_TIME = msciid.CFG_LOW_PWR_IO_LINE_TIMER
		elseif can_cfg == "3" then
			-- Ignition line is not wired - if we're powered, assume it's on 
			IGNITION_SENSE_LINE = "always_on"
			IGNITION_SENSE_TIME = msciid.CFG_LOW_PWR_IO_LINE_TIMER
		end
	elseif string.match( SW_VERSION, "^4.9" ) then
		if devicetree.get( "aleos.59077" ) == "1"
		   and devicetree.get( "aleos.59076" == "2" ) then
			IGNITION_SENSE_LINE = "system.io.gpio.1"
			IGNITION_SENSE_TIME = msciid.CFG_LOW_PWR_IO_LINE_TIMER
		end	
	end

--	log(APP_NAME,"INFO", "Detected ALEOS %s, using %s as ignition trigger",SW_VERSION, IGNITION_SENSE_LINE)
	log( APP_NAME, "INFO", "Detected ALEOS " .. SW_VERSION .. ", using " .. IGNITION_SENSE_LINE .. " as ignition trigger" )

	mqtt = _mqtt(APP_NAME)
	transport = _transport(APP_NAME)
	BnB = _BnB(APP_NAME)

	local bbem = _bbem("127.0.0.1", 0xbb5e)
	if BnB:connect(bbem) then
		log(APP_NAME, "INFO", "Using BBSE Streamer interface (detected)")
	elseif devicetree.get(msciid.CFG_AAF_RESERVE_SERIAL_PORT) == 0 then -- Note: Check passes for 0, not "0".
		log(APP_NAME, "INFO", "Fallback to BBSE Streamer interface (serial port not reserved)")
	else
		bbem:close()
		bbem=nil
		serial_port = serial.open("/dev/ttyS0", {baudRate=115200,flowControl="none"})
		BnB:connect(serial_port)
		log(APP_NAME, "INFO", "Using serial Streamer interface")
	end

	telemetry = _telemetry(APP_NAME, BnB, format_mqtt_msg)
	
	if not transport or not BnB or not telemetry then
		log(APP_NAME, "ERROR", "Cannot initialize")
		return
	end

	telemetry.options = persist.load(CONFIG_ASSET_NAME)
	if not telemetry.options then
		telemetry.options = {}
	end
	BnB:get_default_options(telemetry.options)
	-- load and activate system options
	for soptname, soptval in pairs(default_system_options) do
		if not telemetry.options[soptname] then
			telemetry.options[soptname] = soptval.value
		end
		if soptval.handler then
			soptval.handler(soptname, telemetry.options[soptname])
		end
	end

	-- Set up the asset for configuration
	local st,err = airvantage.init()
	if not st then
		log(APP_NAME, "ERROR", "Cannot initialize av lib %s", err)
		return
	end

	configasset = airvantage.newAsset(CONFIG_ASSET_NAME)
	configasset.tree.downlink = receive_config
	configasset.tree.uplink = amm_request_config	-- for AMM compatibility
	configasset.tree.commands.ReadNode = request_config
	configasset:start()

    local address, uid, passwd, cpack, tstore, kstore, pk = get_mqtt_broker()
    log("AVTA", "INFO", "MQTT connecting to %s", address)
    mqtt:configure {
		address = address,
		cid = DEVICE_ID,
        userid = uid,
		password = passwd,
		certpack = cpack,
		truststore = tstore,
		keystore = kstore,
		privatekey = pk,
		persist=MQTT_PERSIST_NAME,
		topic=MQTT_DATA_PATH,
		assetname=MQTT_ASSET_NAME,
		-- maxqueue=10
	}
	mqtt:run()

	transport:configure {protocol = mqtt}
	transport:run()
	if devicetree.get(msciid.STS_TIME_VALID) == "1" then
		transport:setTimeValid()
	end

	-- trap signal to get app shutdown notification
	psignal.signal("SIGTERM",
		function()
			local now = transport:getValidtime()
			log(APP_NAME, "WARNING", APP_NAME.." SIGTERM Received ...")
			-- remove unclean shutdown marker lastheartbeat
			persist.save("lastheartbeat", nil)
			
			-- save shutdown time and reason. On first execution will generate shutdown event
			-- using this timestamp and code. Note that ALEOS sometimes sends this signal
			-- more than once, so ignore all but first (reset at boot).
			if not persist.load("lastshutdown") then
				persist.save("lastshutdown", {time = now, reason = 0} )
			end
			
			-- signal rest of app to stop
			sched.signal(APP_NAME, "EXIT")
			log(APP_NAME, "WARNING", APP_NAME.." Exiting ...")
		end
	)

	-- generate startup/shutdown events
	checkstartupdown()
	
	local last_heartbeat = 0

	-- Set up WAN state module before first use, then trigger for initial update
	wanstate.init(devicetree,
		function(message) log(APP_NAME, "DEBUG", message) end
	)
	
	-- Register for various operational events
    local registerIds = registertoaleos()
	
	-- Get most up to date WAN status
	updateWanEvents()

	-- For debug access via telnet console
	if DEBUG then
		tt = {
			devicetree = devicetree,
			persist = persist,
			uptime = uptime,
			getevent = getevent,
			generateevent = generateevent,
			split = split,
			transport = transport,
			mqtt = mqtt,
			telemetry = telemetry,
			wanstate = wanstate,
			updateWanEvents= updateWanEvents,
			dlyUpdateWanEvents = dlyUpdateWanEvents,
			WanUpdateFilter = WanUpdateFilter,
			rename = rename,
			send_msg = send_msg,
			configasset = configasset,
			vbistartparms = vbistartparms,
			vbiendparms = vbiendparms,
			active_incident_ids = active_incident_ids,
			default_system_options = default_system_options,
			receive_config = receive_config,
			request_config = request_config,
			incid = incid,
			msciidtoevent = msciidtoevent,
			msciid = msciid,
			registerIds = registerIds,
			-- list of options/values (eg tt.setopt {heartbeat="enable=1, min=120", sys_log_level="DETAIL"} )
			setopt = function(opts) receive_config(configasset, opts, "uplink") end,
			-- list of options to read (eg tt.setopt {"odometer", "engine_speed"} )
			getopt = function(opts)
				for k, v in pairs(opts) do
					opts[k] = "downlink." .. v
				end
				return get_config(opts)
			end,
		}
	end

	telemetry:start()

    TELEM_SCAN_TOOL_STATUS = devicetree.get(msciid.STS_TELEM_SCAN_TOOL_STATUS)
    if (TELEM_SCAN_TOOL_STATUS) then
        if BnB.get_CustomTelemData_enable() == 1 then
            local event = {}
            event["scantool_status"] = TELEM_SCAN_TOOL_STATUS
            send_msg(event)
        end
    end

    local title = devicetree.get(msciid.STS_TELEM_CFG_FILE_TITLE)
    if (title ~= "NOTSET") then
        TELEM_CFG_FILE_TITLE = title
        --log(APP_NAME, "DEBUG", "AVTA-105 - MSCIID_STS_TELEM_CFG_FILE_TITLE already set to "..TELEM_CFG_FILE_TITLE)
    end
    local revision = devicetree.get(msciid.STS_TELEM_CFG_FILE_REVISION)
    if (revision ~= "NOTSET") then
        TELEM_CFG_FILE_REVISION = revision
        --log(APP_NAME, "DEBUG", "AVTA-105 - MSCIID_STS_TELEM_CFG_FILE_REVISION already set to "..TELEM_CFG_FILE_REVISION)
    end
    handleCfgFileDiscovery()
    sched.run(throttleProprietaryTelemetryReporting)

	-- Main loop - sit and poll gps and stuff
	log(APP_NAME, "INFO", "Starting main event loop")

	while true do
		-- Checks the current WAN state, if cellular is the
		-- primary connection and if it's connected.
		local links = wanstate.extractWanLinks()
		local isCellUsed
		local isCellConnected
		if links[1] then
			isCellUsed = links[1].type
		end

		-- The following are polled periodically, but are sent
		-- according to telemetry filter rules
		generateevent("heartbeat")
		generateevent("gpioinput")

		-- WAN transfered/received bytes. [MSCIID_STS_MODEM_SENT, MSCIID_STS_MODEM_RECV]
		-- This is only the Cellular data usage since boot.
		-- Will only send when main WAN is cellular
		if isCellUsed and isCellUsed == 1 then
			generateevent("wanlinktxbytes")
			generateevent("wanlinkrxbytes")
			generateevent("cell_rssi")
			generateevent("lte_rsrp")
			generateevent("lte_rsrq")
			generateevent("lte_sinr")
		end

		-- poll for hardware-based ignition state if no streamer present
		if not telemetry:isPresent() then
			generateevent("ignitionstate")
		end
		local gpsdata = devicetree.get('system.gps.fix')
		if gpsdata and gpsdata ~= 0 then
			generateevent("gpslocation")
			generateevent("gpsspeed")
		end

		local sig = sched.wait(APP_NAME, {"EXIT", 1})
		if sig == "EXIT" then
			break
		end
	end

	print("Exiting " .. APP_NAME .. " cleaning up ...")

	-- Execute orderly shutdown
	telemetry:stop()

	if bbem then
		bbem:close()
	else
		serial_port:close()
	end

	transport:stop()

	mqtt:stop()

    -- go thru registerIds and unregister them all.
    for k,v in pairs(registerIds) do
        devicetree.unregister(v)
        sched.wait(0.1)
    end
    registerIds = {}

    for k,v in pairs(TELEM_CFG_FILE_SIGNAL_REGISTERED_USERID_TBL) do
        devicetree.unregister(v)
        sched.wait(0.1)
    end
    TELEM_CFG_FILE_SIGNAL_REGISTERED_USERID_TBL = {}

	configasset:close()

	os.exit()
end

------------------------------------------------------------------
-- main - Debug shell 
-- 
-- Inputs:
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	Start a telnet server on port 1234
--	Once this program is started, you can start a Lua VM through telnet
--	using the following command: telnet localhost 1234
------------------------------------------------------------------
if DEBUG then
	local function run_server()
	local shell = require 'shell.telnet'
	
	shell.init {
		address     = '127.0.0.1',
		port        = 1235,
		editmode    = "edit",
		historysize = 100 }
	end

	sched.run(run_server)
end

sched.run(main)
sched.loop()
