-------------------------------------------------------------------------------
-- Copyright (c) 2016 Sierra Wireless. All rights reserved.
--
-- Contributors:
--     Randall Pfeifer - Built from AMMER (m3da.lua) initial implementation
--
--	Protocol Send Handler
--	Sends formated via configured protocol.
--	Handles persisting data across reboot in a way that minimizes flash wear.
--	Implements send latency, so that data is send in chunks to help improve efficiency.
--	Witholds sending until system time is known, ensuring all data is correctly timestamped.
-------------------------------------------------------------------------------

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

local persist = require 'persist'
local queue_new = require 'queue'
local lock = require 'sched.lock'
local uptime = require 'sched.timer.core'
local timer = require 'timer'


-- ----------------------------------------------------------------------------
-- CONSTANTS
-- ----------------------------------------------------------------------------
local EVTID_BASE = 64					-- base of event id
local EVTID_PERSIST = 'evtid_base'		-- persist to save current evtid base

local EVTQ_PERSIST = 'evts_queued'		-- persist count & duration of queued events

local DEVT_INFO		= "info"			-- DELS event to inform AMM of info messages
local DEVT_WARN		= "warning"			-- DELS event to inform AMM of warning messages
local DEVT_ERROR	= "error"			-- DELS event to inform AMM of error messages


-- ----------------------------------------------------------------------------
-- Local Functions
-- ----------------------------------------------------------------------------

------------------------------------------------------------------
-- getevtid - get the next event id
-- 
-- Inputs:
--
-- Outputs:
--
-- Returns:
--	event id
--
-- Notes
------------------------------------------------------------------
local function getevtid(self)
	local rtn = self.eventid_base + self.eventid
	self.eventid = self.eventid + 1
	if self.eventid >= EVTID_BASE then
		self.eventid = 0
		self.eventid_base = self.eventid_base + EVTID_BASE
		persist.save(EVTID_PERSIST, self.eventid_base)
	end
	return rtn
end

------------------------------------------------------------------
-- getSystimeMs - get the system time in mS
-- 
-- Inputs:
--
-- Outputs:
--
-- Returns:
--	system time, in mS
--
-- Notes
--	Currently has resolution of 1000 mS, since that's limit of ALEOS
--	os.time. Could synthesize better resolution in future.
--
--	Guarantees that each call returns a unique time, even if multiple
--	calls made within clocks resolution
--
--	If the time is invalid, will return current uptime such that
--	when time becomes valid, we are able to correct the timestamp
--	of past events via the offset.
------------------------------------------------------------------
local function getSystimeMs(self)

	local now

	if self.timeMgr.isValid then
		now = os.time() * 1000

		if now <= self.timeMgr.last_now then
			-- Guarantee that time is unique.
			-- If > 1 call within clock resolution. increment
			-- mS to ensure it is unique
			now = self.timeMgr.last_now + 1
		end
		self.timeMgr.last_now = now
	else
		now = math.floor(uptime.time() * 1000)
	end

	return now
end

------------------------------------------------------------------
-- fixTimeQueue - fixes incorrect timestamps of events in the
--	time queue.
--
-- Inputs:
--	now 	- current valid time
--	flushed - flag to signal if we are to flush the invalidStats
--		table
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	Events with incorrect timestamps are added into a queue and not
--	sent until the time is corrected. Once AVTA is notified that
--	os.time is correct, will fix all the timestamps in the queue
--	by calculating the offset of the current time vs starttime.
--
------------------------------------------------------------------
local function fixTimeQueue (self, now)
	-- Time is now valid. Since all time are based on uptime, the offset is
	-- calculated should be the valid boot time.
	-- Push all of the data with invalid timestamps to AMM, correcting each
	local offset = now - math.floor(uptime.time() * 1000)
	local toFlush = false
	log(self.app_name, "INFO", "Time updated: sending "..self.timeMgr.queue:len().." delayed events")

	-- Messing with the system messes with paho mqtt lib - fails to correctly send
	-- the initial 'catch-up' transmission. Re-start mqtt to avoid this problem.
	self.protocol:stop()
	self.protocol:run()

	while self.timeMgr.queue:len() > 0 do
		toFlush = true
		-- pop event, adjust timestamp, and send it
		local data = self.timeMgr.queue:pop()
		data.timestamp = data.timestamp + offset

		if self:pushRow(data) ~= "ok" then
			log(self.app_name, "DETAIL", "->Event not pushed '"..tostring(data).."'")
		end
	end
	self.timeMgr.invalidStats[ self.timeMgr.currStack ] = {}
	self.timeMgr.starttime = nil
	return toFlush
end

------------------------------------------------------------------
-- checkStat - notifies AMM that there were events lost due to
--	unfixable invalid timestamps.
--
-- Inputs:
--	flushed	- flag to check if invalidStats table should be flushed.
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	Currently only outputs the log message and is not sent to
--	AMM or ALMS.
--	invalidStats keeps a count of the # of events that has been
--	pushed the queue with invalid timestamps. If the timestamps
--	are not corrected before reboot, there is currently no method
--	to fix the timestamp and as such, is lost.
--	invalidStats keeps track of count and timespan, and is saved
--	onto the device.
------------------------------------------------------------------
local function checkStat(self, flushed)
	-- Check for queued bad-time messages, send Miscellanious info message if there are any.
	-- These are created if GW reboots before time becomes valid, so they're lost. Gotta warn AMM.
	if #self.timeMgr.invalidStats[ self.timeMgr.currStack ] > 0 then
		log(self.app_name, "ERROR", "Problem with dropped events list")
	else
		local now = getSystimeMs(self)
		local sent = false
		for i, losses in ipairs(self.timeMgr.invalidStats) do
			local count = losses.count
			if count then
				flushed = true
				local span = losses.last - losses.first
				local msg = "Time unavailable: lost "..count.." events spanning "..span.." mS"
				-- push inform message directly instead of via inform()
				-- to avoid issues with lock/resursion issues calling send() here
				log(self.app_name, "WARNING", msg)
				-- disable sending for now - Bypasses msg config & rename
--					self:pushRow({	timestamp=now, warning = msg})
			end
		end
		if flushed then
			self.timeMgr.invalidStats = { {} }
			self.timeMgr.currStack = #self.timeMgr.invalidStats
			persist.save(EVTQ_PERSIST, self.timeMgr.invalidStats)
		end
	end
end

------------------------------------------------------------------
-- updateTimeQueue - adds event with incorrect timestamp into the
--	timeMgr queue.
--
-- Inputs:
--	data - information of the event, as well as the timestamp.
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	When the time is confirmed to be valid, all the events in the
--	queue will be corrected and then sent.
--
------------------------------------------------------------------
local function updateTimeQueue (self, data)
	-- current time is invalid - enque data so time stamp can be fixed later
	self.timeMgr.queue:push(data)
	if self.timeMgr.queue:len() > self.maxqueue then
		-- Limit queue size by dropping oldest (log it, though)
		local dropped = self.timeMgr.queue:pop()
		local msg = "Too many events queued - dropped: "
		for k,v in pairs(dropped) do msg = msg..k.."="..tostring(v)..", " end
		log(self.app_name, "DETAIL", msg)
	end
end

------------------------------------------------------------------
-- updateTimeStat - updates invalidStats with the count and time
--
-- Inputs:
--	timestamp -	timestamp of the current invalid time.
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	Uses timestamps to keep track of how long the events have been
--	invalid for.
--
------------------------------------------------------------------
local function updateTimeStat (self, timestamp)
	if not self.timeMgr.invalidStats[ self.timeMgr.currStack ].count then
		self.timeMgr.invalidStats[ self.timeMgr.currStack ].count = 1
		self.timeMgr.invalidStats[ self.timeMgr.currStack ].first = timestamp
	else
		self.timeMgr.invalidStats[ self.timeMgr.currStack ].count = self.timeMgr.invalidStats[ self.timeMgr.currStack ].count + 1
	end

	self.timeMgr.invalidStats[ self.timeMgr.currStack ].last = timestamp
	persist.save(EVTQ_PERSIST, self.timeMgr.invalidStats)
	log(self.app_name, "DEBUG", "Invalid time: "..self.timeMgr.invalidStats[ self.timeMgr.currStack ].count.." events spanning "..self.timeMgr.invalidStats[ self.timeMgr.currStack ].last - self.timeMgr.invalidStats[ self.timeMgr.currStack ].first.." seconds delayed")
end

------------------------------------------------------------------
-- checkTimeStamp -	checks if the incoming events already has
--	timestamps.
--
-- Inputs:
--	data - data table of the events.
--		Timestamps are validated before saved so if an event has
--		a timestamp, will assume to be valid.
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	majority of the incoming events will not have timestamps added
--	with a few notable exceptions:
--		"lastshutdown"/"shutdown"
--		"lastheartbeat"/"uncleanshutdown"
--		"startup" (boot = 0)
--		"bootignitionstate" (boot = 0)
--
------------------------------------------------------------------
local function checkTimestamp (self, data)
	-- If data has a timestamp (error checked on creation), then
	-- just needs to push the event.
	if data.timestamp and data.timestamp ~= 0 then
		return true
	else
		if data.timestamp == 0 then
			-- An event marked with a pre-existing timestamp == 0 adjusts timestamp
			-- to indicate boot time instead of app start time
			local ut = math.floor(uptime.time() * 1000)
			data.timestamp = getSystimeMs(self) - ut
			log (self.app_name, "DETAIL", "Started " .. os.date("%X", math.floor(ut / 1000)) .. " after boot")
		else
			-- timestamp not overridden, use current time
			data.timestamp = getSystimeMs(self)
		end
		return self.timeMgr.isValid
	end
end

------------------------------------------------------------------
-- chktime - check if the RTC is valid
--
-- Inputs:
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	Call to check if the RTC is valid, usually once at startup and,
--	if clock not correct then, then again when it becomes valid
--	(via devtree notification).
--
--	When RTC is valid, the function will correct time stamps of
--	events which were generated before when the actual time was known.
------------------------------------------------------------------
local function chktime(self)
	local now = getSystimeMs(self)
	local flushed = false
	-- compare current time to some date in the past to see if clock is valid instead
	-- of relying on MSCIID_STS_TIME_VALID, because there's a small window where where
	-- this flag may not be accurate.
	if not self.timeMgr.isValid then
		-- Time not valid yet (app starting).
		if not self.timeMgr.starttime then
			self.timeMgr.starttime = now
			log(self.app_name, "WARNING", "RTC invalid - withholding transmission until time is known")
		end
--		log(self.app_name, "WARNING", "---------> RTC invalid: "..now)
	else
		if self.timeMgr.starttime then
			flushed = fixTimeQueue(self, now)
		end
		checkStat(self, flushed)
	end
end

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

------------------------------------------------------------------
-- send - Member func to send the data to AMM
-- 
-- Inputs:
--	data		- table of DELS data parameters to send:
--					messageid
--					param0
--					param1
--					extra
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	Sends data if form of set of parameter=value pairs to recieving
--	endpoint via configured protocol object. Tf the data contains a
--	parameter "timestamp" it will override the inserted timestamp.
--	If it's 0, the inserted timestamp will be the gateway's boot time.
--
--	If system time is not currently valid, the event(s) will be held
--	in a queue until the correct time is known, so that the timestamps
--	can be corrected and the events then sent with the correct time.
--	Call with null data when the time becomes valid to send the time-
--	corrected events immediately instead of waiting for next send.

--
--	Also keep a persisted record of the number of enqueued events and
--	the timestamp of 1st and last event. If a reboot occurs before the
--	time becomes known, it is impossible to re-construct the correct
--	timestamps, but an event can be created from this data to notify
--	of the duration and the number of affected events lost (discarded).
------------------------------------------------------------------
local function m_send(self, data)
    if self.running then
		-- Lock to prevent sending events while dumping the chktime queue
		self.lock:acquire()

		-- Check if there are any events whitheld due to invalid time stamps
		chktime(self)

		if type(data) == "table" then
			local eventTimeValid = checkTimestamp (self, data)

			if log.musttrace(self.app_name, "DEBUG") then
				local tmp = "Send:"
				for k,v in pairs(data) do tmp = tmp.."  "..tostring(k).."="..tostring(v) end
				log(self.app_name, "DEBUG", tmp)
			end

			if eventTimeValid then
				if self:pushRow(data) ~= "ok" then
					log(self.app_name, "DETAIL", "->Event not pushed '"..tostring(data).."'")
				end
				--[[
				if log.musttrace(self.app_name, "DEBUG") then
					local msg = "Sent Event: "
					for k,v in pairs(data) do msg = msg..k.."="..tostring(v)..", " end
					log(self.app_name, "DEBUG", msg)
				end
				--]]
			else
				updateTimeQueue (self, data)
				updateTimeStat (self, data.timestamp)
			end

		elseif data ~= nil then
			log(self.app_name, "DETAIL", "->Event not pushed '"..tostring(data).."'")
		end
		self.lock:release()
	end
end

------------------------------------------------------------------
-- m_setValid - sets the time as valid
--
-- Inputs:
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	concurrent function which sets the time as valid.
--	prevents trying to read the flag and reading at the same time.
------------------------------------------------------------------
local function m_setValid (self)
	self.lock:acquire()
	self.timeMgr.isValid = true
	self.lock:release()
end

------------------------------------------------------------------
-- m_getValidtime
--
-- Inputs:
--
-- Outputs:
--
-- Returns:
--	returns the current time in MS if os.time is valid
--	otherwise, returns 'nil' if the time is invalid
--
-- Notes
------------------------------------------------------------------
local function m_getValidtime (self)
	local retval = nil
	if self.timeMgr.isValid then
		retval = getSystimeMs(self)
	end
	return retval
end

------------------------------------------------------------------
-- pushRow - push row of data to AMM
-- 
-- Inputs:
--	data		- table of DELS data parameters to send:
--					timestamp
--					eventid
--					messageid
--					param0
--					param1
--					extra
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	Replacement self:pushRow for (table:pushRow) is used to provide
--	limited functionality of persisted table:pushData() functionality,
--	but with much less flash wear. Attempts to duplicate alogrithm used
--	by oMG:
--	- Send pushed data into a queue (RAM) and start latency timer (via signal) if not already running.
--	- When latency timer triggers, all available messages are 'peeked' from the queue and sent immediately (synchronous)
--	- If the send was successful then the 'peeked' messages are popped and discarded.
--	- If the send failed, send data is cleared, and re-loaded from queue on next send
--	- Peridically, if there are at least n messages in the queue, the queue is persisted to disk.
--	- Upon orderly shutdown the queue is persisted to disk.
--	- On startup, the queue is restored from persist.
------------------------------------------------------------------
local function m_pushRow(self, data)
	-- If max queue size reached send a inform warning. Can infer restoration by next message
	if self.pushqueue:len() == self.maxqueue - 1 then
-- disable sending notification for now - it bypasses msg config & rename. -1 prevents log spamming
--		self.pushqueue:push {
--								timestamp = getSystimeMs(self),
-- --								eventid = getevtid(self),
--								warning = "Max Tx queue size of " .. self.maxqueue .. " reached" }

		log(self.app_name, "WARNING", "Max Tx queue size of %d reached", self.maxqueue)
	end
	
	if self.pushqueue:len() < self.maxqueue then
		self.pushqueue:push(data)
	end
	sched.signal(self.app_name, "SEND")

	return "ok"
end

------------------------------------------------------------------
-- inform - Inform AMM of internal conditions/errors
-- 
-- Inputs:
--	status	- "warning", "error", or "info"
--	fmt		- string format that holds the log text the same way as string.format does
--	varargs	- additional arguments can be provided (as with string.format)
--
-- Outputs:
--
-- Returns:
--
-- Notes
------------------------------------------------------------------
local function m_inform(self, status, fmt, ...)
	local msg = string.format(fmt, ...)
	local evts = {info=DEVT_INFO, warning=DEVT_WARN, error=DEVT_ERROR}
	
	if evts[status] then
		self:send {[evts[status]] = msg}
		log(self.app_name, "INFO", "Inform AMM %s: %s", status, msg)
	else
		log(self.app_name, "WARNING", "Unrecognized inform status %s: %s", status, msg)
	end
end

-- Timer callback to peridically save the Tx queue in case of unexpected power loss.
-- Only save if events are past threshold to prevent minimal unsent data from creating
-- excessive saves. Note that queue save has built-in checks to eleminate redundant writes, too.
local function savequeue(self)
	local size = self.pushqueue:len()
	if (size == 0 or size >= self.persistthresh) and self.pushqueue:save(self.persistname, false) then
		log(self.app_name, "DETAIL", "Persisted %d records to Tx queue", self.pushqueue:len())
	else
		log(self.app_name, "DEBUG", "queue not saved")
	end
end

------------------------------------------------------------------
-- run - Start transport send operations
-- 
-- Inputs:
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	Start transport send operations coroutine.
--	When triggered by "SEND" signal, wait for latency time, and then
--	send all available data from the Tx queue and trigger the
--	send policy to send it. If data is successfully sent, remove it
--	from the Tx queue.
------------------------------------------------------------------
local function m_run(self)
	if self.running then
		log(self.app_name, "WARNING", "Attempted to start already-running transport task")
		return
	end

    self.running = true
	
	self.pushqueue:restore(self.persistname)
	log(self.app_name, "INFO", "Restored %d Persisted records to Tx queue", self.pushqueue:len())

	if self.persistrate > 0 then
		self.persisttimer = timer.periodic(self.persistrate, savequeue, self)
	end
	
	-- Init timestamp check
	self:send(nil)

	self.connect_task = sched.run (
        function()
            repeat
                local signal = sched.wait(self.app_name, "*")
				log(self.app_name, "DEBUG", "transport executing on signal " .. signal)
				if signal == "SEND" then
					sched.wait(self.latency)
					repeat
						local data = self.pushqueue:get(self.maxsend)
						log(self.app_name, "DETAIL", "Pushing %d / %d Records ...", #data, self.pushqueue:len())
						local success, err = self.protocol:send(data)
						if success == "ok" then
							log(self.app_name, "DEBUG", "Send success - drop " .. #data .. " records")
							for i = 1, #data do self.pushqueue:pop() end
						else
							log(self.app_name, "DETAIL", "Send failure (" .. #data .. " events) - " .. err)
							break
						end
					until self.pushqueue:len() == 0 or not self.running
				elseif signal == "SAVE" then
					-- persist queue
				elseif signal == "STOP" then
					self.running = false
				else
					log(self.app_name, "DEBUG", "transport no action on signal " .. signal)
				end
				log(self.app_name, "DEBUG", "transport action complete")
            until not self.running
			self:send("Stopped")
			log(self.app_name, "INFO", "transport task exiting ...")
        end
    )
	sched.wait()	-- yield to start thread
end

------------------------------------------------------------------
-- stop - Stop transport send operations
-- 
-- Inputs:
--
-- Outputs:
--
-- Returns:
--
-- Notes
--	Stops all transport send processes ands immediately persists the
--	Tx queue to flash.
------------------------------------------------------------------
local function m_stop (self)
	if self.running then
		self.running = false
		sched.signal(self.app_name, "STOP")
		if self.persisttimer then
			timer.cancel(self.persisttimer)
			self.persisttimer = nil
		end
		-- Force queue persist on stop. Do here in case triggerPolicy is
		-- blocked trying to send.
		log(self.app_name, "INFO", "Persisting %d records in Tx queue", self.pushqueue:len())
		self.pushqueue:save(self.persistname, true)
	end
end

------------------------------------------------------------------
-- configure - Configure transport parameters
-- 
-- Inputs:
--	config			- table of parameter / value pairs
--
-- Outputs:
--
-- Returns:
--
-- Notes
------------------------------------------------------------------
local function m_configure(self, config)
	-- Object to send data to target endpoint
	if type(config.protocol) == "table" and type(config.protocol.send) == "function" then
		self.protocol = config.protocol
	end
	
	-- Maximum queue size
	if tonumber(config.maxqueue) then
		self.maxqueue = tonumber(config.maxqueue)
	end
	
	-- Maximum events in a message
	if tonumber(config.maxsend) then
		self.maxsend = tonumber(config.maxsend)
	end
	
	-- latency is the send latency in seconds. Items sent within this period will
	-- be sent with the one initiating the send.
	if tonumber(config.latency) then
		self.latency = tonumber(config.latency)
	end
	
	-- set persist rate. 0 cancels automatic persist and triggers persist now
	if tonumber(config.persistrate) then
		self.persistrate = tonumber(config.persistrate)
		
		if self.persisttimer then
			timer.cancel(self.persisttimer)
			self.persisttimer = nil
		end
		if self.running then
			if tonumber(config.persistrate) > 0 then
				self.persisttimer = timer.periodic(tonumber(config.persistrate), savequeue, self)
			else
				savequeue(self)
			end
		end
	end
	
	-- persistthresh sets number of queued items that need to be present to save
	if tonumber(config.persistthresh) then
		self.persistthresh = tonumber(config.persistthresh)
	end
	
	-- persistname is name of persist to save queue
	if config.persistname then
		self.persistname = self.app_name .. tostring(config.persistname)
	end

	-- queuetail is size of queue debug tail (retained stale data)
	if config.queuetrail then
		self.pushqueue:settrail(tonumber(config.queuetrail))
	end
end

------------------------------------------------------------------
-- create - Create an instance of transport
-- 
-- Inputs:
--	app_name		- Name of the app (for logging etc)
--
-- Outputs:
--
-- Returns:
--	transport instance	- sucess
--	nil, error		- failure
--
-- Notes
------------------------------------------------------------------
local function m_create(app_name)
    local m = {}
	local err

    m.app_name = app_name
    m.configure = m_configure
    m.run = m_run
    m.stop = m_stop
    m.send = m_send
	m.inform = m_inform
	m.pushRow = m_pushRow
	
	-- debugging
	m.getSystimeMs = getSystimeMs
	
	-------------------------
	-- Time Stamp functions and variables
	-- A timestamp management table created to help deal with the timestamp
	-- implementation
	m.timeMgr = {
		last_now					= 0,
		isValid						= nil,
		queue						= queue_new(),
		startTime					= nil,
		toFlush						= nil,
		invalidStats				= persist.load(EVTQ_PERSIST),
		currStack					= nil -- This is the number of stacks of event statistics. Same as # of consequtive reboots with events with invalid time.
	}

	m.setTimeValid	= m_setValid
	m.getValidtime	= m_getValidtime

	-- persisted table to hold list of stats describing occurrance of
	-- lost events due to lack of known time prior to reboot.
	-- Upon startup, last stat in the list is initially empty (either
	-- newly created if existing stats or re-used if last is empty).
	-- This last stat will be discarded when the respective queued events
	-- are time-adjusted and sent, or sent after next boot as warning message.
	if m.timeMgr.invalidStats == nil or #m.timeMgr.invalidStats < 1  then
		m.timeMgr.invalidStats = { {} }
		persist.save(EVTQ_PERSIST, m.timeMgr.invalidStats)
		log(app_name, "INFO", "EVTQ_PERSIST initialized")
	end
	m.timeMgr.currStack = #m.timeMgr.invalidStats
	-- Ensure there's a fresh instance slot on top at boot.
	if m.timeMgr.invalidStats [ m.timeMgr.currStack ].count then
		table.insert(m.timeMgr.invalidStats, {})
		m.timeMgr.currStack = m.timeMgr.currStack + 1
		persist.save(EVTQ_PERSIST, m.timeMgr.invalidStats)
		log(app_name, "INFO", "EVTQ_PERSIST new tip added")
	end
	-------------------------

	-- A default protocol object that just displays data to send
	-- (expected to be replaced via configure).
	-- The protocol object is expected to accept list of messages.
	-- This is expected to have at least the following methods. Each message
	-- is a table of one or more key=value pairs, which will be formatted
	-- in a way sutable for that protocol. There is no restriction on the
	-- format of this data item with one exception: it will contain at least
	-- one parameter, timestamp, which is the time the message was created
	-- (system time, in mS) as a number. All pending messages will be passed
	-- in this list, and the protocol object is expected to return "ok" on
	-- successful send of the entire list, or nil, reason of failure.
	m.protocol = {
		send = function(self, data)
			p(self.app_name .. " stub protocol send:", data)
			return "ok"
		end,
		stop = function(self) end,
		run = function(self) end,
		app_name = app_name
	}

    m.running = false
	
	m.lock = lock:new()
	
	-- Upper part of event id is persisted and incremented on boot
	-- or when the lower part exceeds the base. Guarantees unique
	-- event id for every message without too much flash wear.
	m.eventid_base = persist.load(EVTID_PERSIST)
	if type(m.eventid_base) ~= 'number' then
		m.eventid_base = 0	-- ToDo: make event id persistent
	else
		m.eventid_base = m.eventid_base + EVTID_BASE
	end
	persist.save(EVTID_PERSIST, m.eventid_base)
	m.eventid = 0

	-- table for transfer (Tx queue)
	m.pushqueue = queue_new()

	-- Default transport send parameters
	m.maxqueue = 10000
	m.persistthresh = 1
	m.persistrate = 0
	m.latency = 5
	m.maxsend = 0
	m.persistname = app_name .. "_send_queue"
    return m
end

return m_create

