-------------------------------------------------------------------------------
-- Copyright (c) 2015 Sierra Wireless. All rights reserved.
--
-- Contributors:
--     Randall Pfeifer - initial implementation
--
--	Interface to B&B OBDII Streamer Module
--	Handles sending/receiving data to the B&B Streamer module, formatting
--	and organizing queries and responses, handling firmware updates. 
-------------------------------------------------------------------------------

-- ----------------------------------------------------------------------------
-- REQUIRES
-- ----------------------------------------------------------------------------
local bit = require 'bit32'

-- ----------------------------------------------------------------------------
-- CONSTANTS/DATA
-- ----------------------------------------------------------------------------
local TIMEOUT = 1

-- B&B Command definitions
local commands = {
	READ_MODEL_NUMBER = 0x02,
	GET_COMPONENT_VERSIONS = 0x03,
	READ_SERIAL_NUMBER = 0x07,
	GET_SUPPORTED_PARAMETERS = 0x20,
	GET_PARAMETER = 0x22,
	GET_VEHICLE_STATUS = 0x23,
	REDETECT_VEHICLE = 0x24,
	GET_VEHICLE_INFO = 0x25,
	UPDATE_COMPONENT = 0x57
}

local status = {
	DEVICE_CONFIGURED = 0x80,
	VEHICLE_NOT_DETECTED = 0x81,
	IGNITION_OFF = 0xD0,
}
	

------------------------------------------------------------------
-- d2i - collect response bytes into an integer 
-- 
-- Inputs:
-- 	strData			- The response data to convert
--
-- Returns:
-- 	Integer value of data (big-endian)
--
-- Notes
--	Extract integer from raw data field (from scanner)
--	Field is in form if a string containing data bytes.
--	Length of string determines size of conversion
------------------------------------------------------------------
local function d2i(strData)
	local val = 0
	for i = 1,string.len(strData) do
		val = val * 256 + string.byte(strData, i)
	end
	return val
end

------------------------------------------------------------------
-- Helper Functions
------------------------------------------------------------------

-- Convert a string into list of byte values
function string2list(str)
	local function a2t(...) local rtn = {...} return rtn end
	return a2t(string.byte(str, 1, -1))
end

-- Convert a list of byte values into a string
function list2string(table)
	return  string.char(unpack(table))
end

-- Round a number to specified # decimal places
function round(num, numDecimalPlaces)
  local mult = 10^(numDecimalPlaces or 0)
  return math.floor(num * mult + 0.5) / mult
end


------------------------------------------------------------------
--                     Data Scaling Functions
--
-- These are used by the PIDInfo table to scale raw data from
-- Streamer into usable values
------------------------------------------------------------------

-- No processing - return number (integer) extracted from data
local function s_none (pidinfo, data)
	return d2i(data)
end

-- extract number (integer) from data, then multiply by scale (double)
local function s_double (pidinfo, data)
	return d2i(data) * pidinfo.scale
end	

-- A boolean value (0 or 1). neg will invert
local function s_bool (pidinfo, data)
	local rtn = d2i(data)
	if pidinfo.neg then
		if rtn == 0 then
			rtn = 1
		else
			rtn = 0
		end
	end
	return rtn
end	

-- Same as s_double, but round off to integer
local function s_int (pidinfo, data)
	return round(s_double (pidinfo, data))
end

-- Same as s_double, but round off to 1 decimal
local function s_number (pidinfo, data)
	return round(s_double (pidinfo, data), 1)
end

-- Convert to a Fahrenheit temperature (all temperature scaling same on B&B)
local function s_temperatureF (pidinfo, data)
	return math.floor((d2i(data) * (1/64) - 40) + 0.5)
end

-- Convert to a Celcius temperature
local function s_temperatureC (pidinfo, data)
	return math.floor(((s_temperatureF(pidinfo, data) - 32) / 1.8) + 0.5)
end

-- Convert data to a list of integer values
-- one for each data byte
local function s_tires (pidinfo, data)
	local rtn = {}
	for i = 1,string.len(data) do
		rtn[i] = string.format("%d", string.byte(data, i))
	end

	return rtn
end

------------------------------------------------------------------
--                     Data Comparison Functions
--
-- These are used by the PIDInfo table to compare values against
-- the threshold for different data types. They compare the given
-- current and last values against given threshold and return true
-- if threshold exceeded, else false. For some, if soz (send on zero)
-- is true, always trigger theshold if value has become 0
------------------------------------------------------------------

-- External threshold - always triggers ( use with externally triggered events)
local function c_ext (threshold, current, last, soz)
	return true
end

-- Numerical comparison - respects soz
local function c_num (threshold, current, last, soz)
	if soz and current == 0 and last ~= 0 then
		-- soz enabled and hit zero, return flag to indicate that...
		return true, true
	end
	return threshold > 0 and (current >= last + threshold or current <= last - threshold)
end

-- Bit-wise comparison
local function c_bit (threshold, current, last, soz)
	return bit.band(current, threshold) ~= bit.band(last, threshold)
end

-- String comparison
local function c_str (threshold, current, last, soz)
	return current ~= last
end

-- Tire Monitor comparison
-- If threshold > 0, then trigger on change of any flag
local function c_trm (threshold, current, last, soz)
	if threshold > 0 then
		for i in ipairs(current) do
			if current[i] ~= last[i] then
				return true
			end
		end
	end
	return false
end

-- Tire Pressure comparison
-- If threshold > 0, then trigger on change of any tire > threshold
local function c_trp (threshold, current, last, soz)
	if threshold > 0 then
		for i in ipairs(current) do
			if (current[i] >= last[i] + threshold or current[i] <= last[i] - threshold) then
				return true
			end
		end
	end
	return false
end

-- GPS location comparison
-- GPS data is a table of latitude & longitude value
-- Return true if either has exceeded threshold
local function c_gps (threshold, current, last, soz)
	-- Calculate the greate circle distance between two points
	-- using the Haversine Formula, and compare to threshold
	-- from http://andrew.hedges.name/experiments/haversine/
	
	if type(last) ~= "table" then
		return false
	end

	local R = 6371000	-- radius of the Earth, in meters (unit of comparison)

    local lat1 = math.rad(current.latitude)
    local lat2 = math.rad(last.latitude)
    local lon1 = math.rad(current.longitude)
    local lon2 = math.rad(last.longitude)

    local dlon = lon2 - lon1
    local dlat = lat2 - lat1
    local a = (math.sin(dlat/2))^2 + math.cos(lat1) * math.cos(lat2) * (math.sin(dlon/2))^2
    local c = 2 * math.atan2( math.sqrt(a), math.sqrt(1-a) )
    local d = R * c -- (where R is the radius of the Earth)

	return threshold > 0 and d >= threshold
end

-- ----------------------------------------------------------------------------
-- TELEMETRY PARAMETER DEFINITION TABLE
-- ----------------------------------------------------------------------------
local null_dflt = {en=0,	th=0,		mn=0,	mx=0}
local bool_dflt = {en=0,	th=1,		mn=0,	mx=0}	
-- Table to associate PIDs with parameters
-- This table contains all information related to configurable parameters. Below are fields
--	index	- temeletry parameter's PID, (or event numbers for others)
--	name	- the name of the parameter used throughout to refer to this parameter
--	size	- size of telemetry data in B&B interface protocol
--	func	- function to extract the specific parameter type from B&B protocol
--	scale	- scaling factor for telemetry parameter (used by func)
--	soz		- flag to indicate send if value becomes zero (regardless of threshold)
--	cmp		- comparison function for parameter datatype to detect threshold
--	dflt	- default configuration values
--	mask	- config values to mask from view (always use default)
local PIDInfo = {
	[0x00]={	name="vehicle_speed",					size=2,	func=s_int,			scale=1/410,	soz=true,	cmp=c_num,	dflt = {en=1,	th=5,		mn=60,	mx=5} },
	[0x01]={	name="engine_speed",					size=2,	func=s_int,			scale=1/4,		soz=true,	cmp=c_num,	dflt = {en=1,	th=100,		mn=60,	mx=5} },
	[0x02]={	name="throttle_position",				size=2,	func=s_int,			scale=1/655,				cmp=c_num,	dflt = null_dflt },
	[0x03]={	name="odometer",						size=4,	func=s_none,									cmp=c_num,	dflt = {en=1,	th=2,		mn=900,	mx=5} },
	[0x04]={	name="fuel_level",						size=2,	func=s_int,			scale=1/655,				cmp=c_num,	dflt = {en=1,	th=2,		mn=120,	mx=5} },
	[0x07]={	name="engine_coolant_temp",				size=2,	func=s_temperatureF,							cmp=c_num,	dflt = {en=1,	th=10,		mn=900,	mx=5} },
	[0x08]={	name="ignition_status",					size=2,	func=s_bool,		neg=true,					cmp=c_num,	dflt = {en=1,	th=1,		mn=0,	mx=0}, mask={th=true} },
	[0x09]={	name="mil_status",						size=2,	func=s_bool,		neg=true,					cmp=c_num,	dflt = {en=1,	th=1,		mn=0,	mx=0}, mask={th=true} },
	[0x0c]={	name="fuel_rate",						size=2,	func=s_number,		scale=1/2185,				cmp=c_num,	dflt = null_dflt },
	[0x0d]={	name="battery_voltage",					size=2,	func=s_number,		scale=1/3641,				cmp=c_num,	dflt = null_dflt },
	[0x0e]={	name="pto_status",						size=2,	func=s_none,									cmp=c_num,	dflt = {en=1,	th=1,		mn=0,	mx=0}, mask={th=true} },
	[0x0f]={	name="seatbelt_fastened",				size=2,	func=s_bool,		neg=true,					cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x10]={	name="misfire_monitor",					size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x11]={	name="fuel_system_monitor",				size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x12]={	name="comprehensive_component_monitor",	size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x13]={	name="catalyst_monitor",				size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x14]={	name="heated_catalyst_monitor",			size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x15]={	name="evaporative_system_monitor",		size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x16]={	name="secondary_air_system_monitor",	size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x17]={	name="ac_system_refrigerant_monitor",	size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x18]={	name="oxygen_sensor_monitor",			size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x19]={	name="oxygen_sensor_heater_monitor",	size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x1a]={	name="egr_system_monitor",				size=2,	func=s_none,									cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x1b]={	name="brake_switch_status",				size=2,	func=s_bool,		neg=true,					cmp=c_num,	dflt = bool_dflt, mask={th=true} },
	[0x1c]={	name="ambient_air_temperature",			size=2,	func=s_temperatureF,							cmp=c_num,	dflt = {en=1,	th=2,	mn=600,	mx=5} },
	[0x22]={	name="trip_odometer",					size=4,	func=s_number,		scale=1/10,					cmp=c_num,	dflt = {en=0,	th=2,	mn=900,	mx=5} },
	[0x23]={	name="trip_fuel_consumption",			size=4,	func=s_int,		scale=1/128,					cmp=c_num,	dflt = {en=0,	th=2,	mn=900,	mx=5} },
	[0x24]={	name="distance_since_dtc_cleared",		size=4,	func=s_none,									cmp=c_num,	dflt = {en=1,	th=2,	mn=0,	mx=5} },
	[0x25]={	name="transmission_fluid_temperature",	size=2,	func=s_temperatureF,							cmp=c_num,	dflt = null_dflt },
	[0x26]={	name="oil_life_remaining",				size=2,	func=s_number,		scale=1/500,				cmp=c_num,	dflt = null_dflt },
	[0x27]={	name="tire_pressure_monitoring_status",	size=8,	func=s_tires,									cmp=c_trm,	dflt = bool_dflt, mask={th=true} },
	[0x28]={	name="tire_pressures",					size=6,	func=s_tires,									cmp=c_trp,	dflt = null_dflt },
	-- VIN, DTCs and GPS are not normal PIDs
	-- Keep them in this table anyway, and assign dummy PIDs (> 100)
	[0x100]={	name="supported_pids",																			cmp=c_str,	dflt = {en=0,	th="",	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
	[0x101]={	name="vin",																						cmp=c_str,	dflt = {en=1,	th="",	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
	[0x102]={	name="dtcs",																					cmp=c_str,	dflt = {en=1,	th="",	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
	[0x103]={	name="gwinfo",																					cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
	-- Synthesized telemetry parameters
	[1031]={	name="engine_run_time",																			cmp=c_num,	dflt = {en=1,	th=60,	mn=0,	mx=0} },
	-- Parameters outside telemetry - filter only
	[70145]={	name="acceleration_incident_start",																cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true} },
	[70146]={	name="acceleration_incident_end",																cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true} },
	[70147]={	name="deceleration_incident_start",																cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true} },
	[70148]={	name="deceleration_incident_end",																cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true} },
	[70149]={	name="cornering_incident_start",																cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true} },
	[70150]={	name="cornering_incident_end",																	cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true} },
--	[70150]={	name="crash_incident_start",																	cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true} },
--	[70151]={	name="crash_incident_end",																		cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true} },
	[70152]={	name="dreckon_sensor_calibration",																cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
	[70153]={	name="cell_rssi",																				cmp=c_num,	dflt = {en=1,	th=10,	mn=600,	mx=5} },
	[70154]={	name="lte_rsrp",																				cmp=c_num,	dflt = {en=1,	th=10,	mn=600,	mx=5} },
	[70155]={	name="lte_rsrq",																				cmp=c_num,	dflt = {en=1,	th=2,	mn=600,	mx=5} },
	[70156]={	name="lte_sinr",																				cmp=c_num,	dflt = {en=1,	th=2,	mn=600,	mx=5} },
	[270]={		name="heartbeat",																				cmp=c_num,	dflt = {en=1,	th=0,	mn=60,	mx=0}, mask={th=true, mx=true} },
	[1000]={	name="gateway_start_stop",																		cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
	[1001]={	name="gpioinput",																				cmp=c_bit,	dflt = {en=1,	th=255,	mn=0,	mx=0} },
	[1002]={	name="wanstatus",																				cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
	[1003]={	name="primarywan",																				cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
	[1004]={	name="vpnstatus",																				cmp=c_ext,	dflt = {en=1,	th=0,	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
	[1005]={    name="carrier",                                                                                 cmp=c_str,  dflt = {en=1,   th="",   mn=0,   mx=0}, mask={th=true, mn=true, mx=true} },
	[1006]={    name="cellrat",                                                                                 cmp=c_str,  dflt = {en=1,   th="",   mn=0,   mx=0}, mask={th=true, mn=true, mx=true} },
	[1007]={    name="celllteband",                                                                             cmp=c_str,  dflt = {en=1,   th="",   mn=0,   mx=0}, mask={th=true, mn=true, mx=true} },
	[520]={		name="wanlinktxbytes",																			cmp=c_num,	dflt = {en=1,	th=500,	mn=0,	mx=30} },
	[521]={		name="wanlinkrxbytes",																			cmp=c_num,	dflt = {en=1,	th=500,	mn=0,	mx=30} },
	[773]={		name="gpsspeed",																	soz=true,	cmp=c_num,	dflt = {en=1,	th=5,	mn=60,	mx=5} },
	[772]={		name="gpslocation",																				cmp=c_gps,	dflt = {en=1,	th=20,	mn=300,	mx=5} },
	[774]={		name="gpsfix",																					cmp=c_str,	dflt = {en=1,	th="",	mn=0,	mx=0}, mask={th=true, mn=true, mx=true} },
    -- Operating parameters for Custom Telemetery Data reporting
	[99915]={	name="custom_telem_data",																		cmp=c_num,	dflt = {en=1,	th=0,	mn=0,	mx=60}, mask={th=true, mn=true} },
}

-- Generate an alternate table to access PID info by name
-- Insert the PID's numerical index to be able
-- to convert between them easily
local PIDInfoByName = {}
for i,p in pairs(PIDInfo) do
	PIDInfo[i].num = i
    PIDInfoByName[p.name] = p
end

-- ----------------------------------------------------------------------------
-- METHODS
-- ----------------------------------------------------------------------------

------------------------------------------------------------------
-- connect - Connects a serial interface to B&B Interface
-- 
-- Inputs:
-- 	port			- serial port object
--
-- Returns:
--	true if streamer is detected
--
-- Notes
--	Attaches serial interface to use for communicating with streamer.
--	Also detects whether a streamer is attached and responding (can
--	be used to determine if MP70 on-board streamer emulation is present)
------------------------------------------------------------------
local function l_connect (self, port)
    self.serial_port = port
    return self:query(string.char(commands.READ_MODEL_NUMBER)) ~= nil
end

------------------------------------------------------------------
-- send - send a command frame to B&B Streamer
-- 
-- Inputs:
-- 	strCommand		- Command data bytes as a string
-- 	strData			- Extra data bytes as a string
--
-- Returns:
--
-- Notes
-- 	Sends a properly formated command frame to the B&B Streamer port
------------------------------------------------------------------
local function l_send(self, strCommand, strData)
    local strResult = string.char(1)..string.char(string.len(strCommand))..strCommand

    if(type(strData) == "string") then
        strResult = strResult..string.char(string.len(strData))..strData
    else
        strResult = strResult..string.char(0)
    end

    local cs = 0
    for i = 1, string.len(strResult) do
        cs = bit.band(cs + string.byte(strResult, i), 0xff)
    end
    self.serial_port:write(strResult..string.char(cs))
end

------------------------------------------------------------------
-- get_response - Get raw response data from B&B
-- 
-- Inputs:
-- 	check			- (optional) If true, expect frame in serial buffer
--
-- Returns:
--	table of:
--		command		= command bytes in response as string
--		data		= data bytes in response as string
--	or
--	nil, error
--
-- Notes
-- 	If check flag is true, disable timeout when looking for SoF,
-- 	so that return is immediate if no frames are buffered in stream
-- 	(no error code or data returned if no data frames detected)
------------------------------------------------------------------
local function l_get_response (self, check)
    -- read single bytes from port, looking for Start of Frame byte
    local serial = self.serial_port
	local cs
    local retval = {}

    if check then
        serial:settimeout(0)
        repeat
            -- Read bytes until SoF or none
            local x = serial:read(1)
            if x == nil then
                return nil
            end
        until string.byte(x,1) == 0x01
    else
        serial:settimeout(TIMEOUT)
        repeat
            -- Read bytes until SoF or timeout
            local x = serial:read(1)
            if x == nil then
                -- No SoF found
                serial:settimeout(0)
                return nil, "timeout(1)"
            end
        until string.byte(x,1) == 0x01
    end
    
    cs = 0x01
    
    --  get control length
    x = serial:read(1)
    if x == nil then
        serial:settimeout(0)
        return nil, "timeout(2)"
    end
    cs = bit.band(cs + string.byte(x, 1), 0xff)
    local len = bit.band(string.byte(x,1), 0x7f)
    
    -- Get the control bytes
    x = serial:read(len)
    if x == nil then
        serial:settimeout(0)
        return nil, "timeout(3)"
    end
    retval.command = x
    
    for i = 1,len do
    	cs = bit.band(cs + string.byte(x, i), 0xff)
    end

    -- get data length
    x = serial:read(1)
    if x == nil then
        serial:settimeout(0)
        return nil, "timeout(4)"
    end
    
    cs = bit.band(cs + string.byte(x, 1), 0xff)
    
    -- Get data bytes
    local len = bit.band(string.byte(x,1), 0x7f)
    x = serial:read(len)
    if x == nil then
        serial:settimeout(0)
        return nil, "timeout(5)"
    end
    retval.data = x

    for i = 1,len do
	    cs = bit.band(cs + string.byte(x, i), 0xff)
    end

    -- get checksum
    x = serial:read(1)
    if x == nil then
        serial:settimeout(0)
        return nil, "timeout(4)"
    end
    
    if cs ~= string.byte(x, 1) then
    	return nil, "checksum"
    end

    return retval
end

------------------------------------------------------------------
-- query - Query B&B and collect response
-- 
-- Inputs:
-- 	strCommand		- Command data bytes as a string
-- 	strReq			- Optional extra (request) bytes as a string
--
-- Returns:
--	table of:
--		command		= command bytes in response as string
--		data		= data bytes in response as string
--	or
--	nil, error
--
-- Notes
--	Send a query and wait for and collect response from B&B.
--	If query is GET_PARAMETER and more than maximum (11) parameters requested,
--	break into multiple queries and combine the results.
------------------------------------------------------------------
local function l_query(self, strCommand, strReq)
--	local cmd = strCommand
	local req = strReq or ""
	local rtn = {}
	rtn.data = ""

	repeat
		self:send(strCommand, string.sub(req, 1, 11))
		if log.musttrace(self.app_name, "DEBUG") then
			log(self.app_name, "DEBUG", "Q: %s    %s", showdata(strCommand), showdata(string.sub(req, 1, 11)))
		end
		local response,errcd = self:get_response()
		if response then
			rtn.command = response.command
			rtn.data = rtn.data .. response.data
			-- Raw response data
			-- print(string.format("Response: %s:%s", showdata(response.command), showdata(response.data)))
		else
			log(self.app_name, "DEBUG", "A: %s", errcd)
			return response, errcd
		end
		req = string.sub(req, 12 , -1)
	until string.byte(strCommand, 1) ~= commands.GET_PARAMETER or string.len(req) == 0
	if log.musttrace(self.app_name, "DEBUG") then
		log(self.app_name, "DEBUG", "A: %s    %s", showdata(rtn.command), showdata(rtn.data))
	end
	return rtn
end

------------------------------------------------------------------
-- process_response - Process B&B Responses 
-- 
-- Inputs:
-- 	response			- Data table from query (command, data)
-- 	output				- Table to add output to 
--
-- Returns:
--
-- Notes
-- 	Interprets data returned from B&B Streamer (as table of command
-- 	and data strings) and parses into output table as parameter/value
-- 	pairs.
------------------------------------------------------------------
local function l_process_response (self, response, output)
	if type(response) == "table" then
		local command = bit.band(string.byte(response.command, 1), 0x7f)
		if command == commands.GET_PARAMETER then
			-- Iterate thru parameter query data, format, and add to response list
			local data_posn = 1
			while data_posn <= string.len(response.data) do
				local PIDInfo = PIDInfo[string.byte(response.data, data_posn)]
				if PIDInfo then
				--	print("***data:"..showdata(string.sub(response.data, data_posn + 1, data_posn + 1 + PIDInfo.size)))
					output[PIDInfo.name] = PIDInfo.func(PIDInfo, string.sub(response.data, data_posn + 1, data_posn + PIDInfo.size))
				--	print("***"..PIDInfo.name.."="..data[PIDInfo.name])
					data_posn = data_posn + 1 + PIDInfo.size
				else
					log(self.app_name, "ERROR", "Process: Invalid PID encountered (0x%02x) at position %d - can't continue", string.byte(response.data, data_posn), data_posn)
					break
				end
			end
		elseif command == commands.GET_VEHICLE_STATUS then
--			print "process: GET_VEHICLE_STATUS - Ignition"
			output.ignition_status = string.byte(response.data, 1)
		elseif command == commands.GET_VEHICLE_INFO then
--			--print "process: GET_VEHICLE_INFO"
			if string.byte(response.data, 1) == 0x00 then
--				--print "process: GET_VEHICLE_INFO - VIN"
				output.vin = string.sub(response.data, 2, -1)
				if output.vin:gsub('%W', ''):len() == 0 then
					-- Don't generate VIN if there are no printable chars in it
					log(self.app_name, "WARNING", "Invalid VIN recieved from streamer: %s", output.vin)
					output.vin = nil
				end
			elseif string.byte(response.data, 1) == 0x02 then
--				--print "process: GET_VEHICLE_INFO - DTCs"
				local dtcs = ""
				local i = 2
				local l = string.len(response.data)
				while i <= l do
					if dtcs ~= "" then
						dtcs = dtcs .. ","
					end
					dtcs = dtcs .. string.sub(response.data, i, i + 4)
					i = i + 5
				end
				output.dtcs = dtcs
			end
		elseif command == 0x50 then
			-- Ignition off Status Message
			output.ignition_status = 0
		elseif command == 0x7f then
			log(self.app_name, "WARNING", "BnB Data Error Response 0x%02x", string.byte(response.command, 2))
		else
			log(self.app_name, "WARNING", "Unhandled BnB Data type 0x%02x", command)
		end
	end
--	return data
end

------------------------------------------------------------------
-- get_default_options - Get a full telemetry options table of defaults 
-- 
-- Inputs:
-- 	force				- (optional) if specified, re-initialize table
--
-- Outputs
-- 	options				- Table to update
--
-- Returns:
--	table of options, for each telemetry parameter, by Name
--
-- Notes
--	Overlays options table with defaults if options for a parameter
--	is missing. Existing options are left untouched. Call with empty
--	options table to generate full set of defaults.
--	
--	Won't affect existing config data but ensures that the entire
--	table is filled in with at least default data (so no need to
--	constantly check for missing data). 
------------------------------------------------------------------
local function l_get_default_options(self, options)
	-- Load options with defaults
	for k,v in pairs(self.PIDInfoByName) do
		if type(options[v.name]) ~= "table" then
			options[v.name] = {}
		end

		for o,s in pairs(v.dflt) do
			if not options[v.name][o] then
				options[v.name][o] = s
			end
		end
	end
end

-- Parse an option setting for param from string in str into options
------------------------------------------------------------------
-- set_option - Parse option string from AVMS into options table 
-- 
-- Inputs:
-- 	param			- name of the parameter to configure
-- 	str				- config string from AVMS
--
-- Outputs:
-- 	options			- config data table containing options
--
-- Returns:
--
-- Notes
--	Parses the configuration string for a parameter and replaces the
--	setting in the options table with the new data. Data format is
--	specified as option=value, and is parsed by Lua itseld in a sandboxed
--	environment to prevent security issues. All four options (en, th, mn, mx)
--	for the parameter are set; unspecified options are set to default
--	(not retained from previous setting). Un-recognized options cause
--	error. option name is not case sensitive.
------------------------------------------------------------------
local function l_set_option(self, options, param, str)
	
	local errcd = nil
	local newcfg

	-- Use Lua interpreter to parse the user-supplied config strings
	-- (param=value pairs, just like table declarations).
	f = (loadstring(string.format("return {%s}", str)))
	if f then
		-- exec with empty env to sandbox it
		local env = {}
		setfenv(f, env)
		local ok, val = pcall(f)
		if ok then
			if not self.PIDInfoByName[param] then
				errcd = string.format("Unknown parameter %s", param)
			else
				-- Get a copy of defaults for this parameter to use as new settings.
				-- Then overlay with new values from parsed settings
				local dflt = self.PIDInfoByName[param].dflt
				newcfg = {en=dflt.en, th=dflt.th, mn=dflt.mn, mx=dflt.mx}

				-- AVMS parameter names. Keep separate from internal names, so it's
				-- possible to have alternate/redundant names
				local pnames = {enable="en", threshold="th", min="mn", max="mx"}

				for k,v in pairs(val) do
					if pnames[string.lower(k)] then
						if type(newcfg[pnames[string.lower(k)]]) ~= type(v) then
							errcd = string.format("Type mismatch for %s in setting for %s", k, param)
							break
						else
							newcfg[pnames[string.lower(k)]] = v
						end
					else
						errcd = string.format("Unrecognized parameter %s in setting for %s", k, param)
						break
					end
				end
			end
		else
			errcd = string.format("Error parsing: %s for parameter %s", str, param)
		end
	else
		errcd = string.format("Bad option: %s for parameter %s", str, param)
	end
	
	if errcd then
		return errcd
	else
		options[param] = newcfg
		return "ok"
	end
end
	
-- Table of version info expected and needed for this release
local fw_table = {
    {name="FPGA",       ver={2,1,46},   offset=1,	srec=true,		file="FPGA_Firmware_Only_v_2.1.46.flash"},
    {name="Database",   ver={2,10,14},    offset=7,	srec=true,		file="V2_DB_2_10_14.flash"},
    {name="System Mgr", ver={2,2,1},    offset=10,	srec=false,		file="EMM_Mainapp_2.2.1.hex"},
    {name="Bootloader", ver={2,0,1},    offset=13,	srec=true,		file="EMM_Bootloader_Update_2.0.1.flash"}
}

-- Update a B&B firmware component
-- Accepts index into fw_table to identify component to update
-- returns "ok" on success, nil, error on failure
local function l_update_component(self, component)
    -- Timing values as specified by OBDII Streamer Command & Response documentation
    local T1 = 25
    local T2 = 35
    local T3 = 45
    local T4 = 45

    local filepath = "LDVDSV2_fw/"

	-- Open file first - make sure it's there before continuing
    local f,err = io.open(filepath..fw_table[component].file, "r")
    if not f then
        return nil, err
    end

    local serial = self.serial_port

	-- clean out junk from serial stream
	repeat
	until not self:get_response(true)

    log(self.app_name, "INFO", "Updating component %s", component)
    self:send(string.char(0x57), string.char(component))
    serial:settimeout(T1)
    response = serial:read(5)     -- 5 bytes expected in response

    if response ~= string.char(0x01, 0x01, 0xd7, 0x00, 0xd9) then
        return nil, "Host command error"
    end
    
    serial:settimeout(T2)
    response = serial:read(4)   -- wait for "ready" string
    if response ~= "@@&&" then
        return nil, "Streamer not ready"
    end
    
    log(self.app_name, "DEBUG", "Sending file %s", filepath..fw_table[component].file)

	-- Send the file, record-record, to the streamer, waiting for
	-- "#" (ack) or "!" (end) between each
    -- Re-pack the record as binary  - keep the initial char (S or :)
    -- and convert ASCII hex nibble pairs to binary
	local r = 0
    while true do
    	local strLine, strLen, strSend
    	local collected = {}

        strLine = f:read("*line")
        if not strLine then
        	break
        end
        strLen = string.len(strLine)

		-- Pick up Start of Record char - don't convert to binary
        table.insert(collected, string.byte(string.sub(strLine, 1, 1)))

		local start = 2	-- intel format start of data
        if fw_table[component].srec then
        	-- srec has 1 extra nibble (srtec type) to insert in binary record
	        table.insert(collected, tonumber("0x0"..string.sub(strLine, 2, 2)))
	        start = 3
	    end

--        print("strLine:", strLen, strLine)
		if r % 100 == 0 then
			log(self.app_name, "DEBUG", "sent line %d", r)
		end
		r = r + 1

        for iter = start, strLen - 1, 2 do
        	table.insert(collected, tonumber("0x"..string.sub(strLine, iter, iter + 1)))
        end

        strSend = list2string(collected)
        serial:write(strSend.."\r\n")

		-- Collect ack character
        local c = serial:read(1)
        if c == "!" then
        	-- ! means last record recieved 
            break
        elseif c ~= "#" then
        	-- # is expected ack - fail if not recieved
        	-- ToDo: re-send record a few more times
            log(self.app_name, "ERROR", "File send ERROR: Incorrect record Ack %s", c)
            break
        end
    end
    f:close()

    for i = 1,T4 do
    	-- Wait for Streamer to recover from update operation
        sched.wait(1)
        local response, err = self:query(string.char(commands.GET_COMPONENT_VERSIONS))
        if response then
            return "ok"
        end
    end
    return nil, "Failed to restart"
end

-- Reset BB streamer trip data to zero
--
local function l_trip_reset( self )
	log(self.app_name, "INFO", "Resetting BB trip data")

	local response, err = self:query(string.char(0x61))

	if err then
		return nil, "Reset command failed"
	end

	log(self.app_name, "INFO", "Response: %s", response.data )

	-- BB will return this if it received the command and everything is ok
    if response.data ~= string.char(0x01, 0x01, 0xe1, 0x00, 0xe3) then
		-- BBEMU will return this because it doesn't support the trip reset command
		if response.data ~= string.char(0x01,0x02,0xFF,0x01,0x00,0x3) then
			-- This is an actual error
        	return nil, "Unknown response to trip reset"
    	end
	end

end

local function l_get_CustomTelemData_enable()

    local pidnum = PIDInfoByName.custom_telem_data.num
    if PIDInfo[pidnum].en then
        return PIDInfo[pidnum].en
    else
        return PIDInfo[pidnum].dflt.en
    end
end

local function l_get_CustomTelemData_max()

    local pidnum = PIDInfoByName.custom_telem_data.num
    if PIDInfo[pidnum].mx then
        return PIDInfo[pidnum].mx
    else
        return PIDInfo[pidnum].dflt.mx
    end
end

local function create(app_name)
	local BnB = {}
	BnB.app_name = app_name
	BnB.send = l_send
	BnB.get_response = l_get_response
	BnB.connect = l_connect
	BnB.query = l_query
	BnB.process_response = l_process_response
	BnB.get_default_options = l_get_default_options
	BnB.set_option = l_set_option
	BnB.update_component = l_update_component
	BnB.fw_table = fw_table
	BnB.trip_reset = l_trip_reset
	
	-- Helper functions
	--BnB.data_to_int = l_data_to_int
	
	-- Data lookup tables
	BnB.PIDInfoByNum = PIDInfo
	BnB.PIDInfoByName = PIDInfoByName
	BnB.commands = commands
	BnB.status = status

    -- Accessor functions for Custom Telemetry Data operating parameters
    BnB.get_CustomTelemData_enable = l_get_CustomTelemData_enable
    BnB.get_CustomTelemData_max    = l_get_CustomTelemData_max

	return BnB
end

return create
