-- benthos.lua
--
-- This is a collection of functions meant to interface with a Benthos
-- acoustic modem.

function Acked(s)

	local result = false	

	if s ~= nil then

		local slen = string.len(s)

		if (slen >= 3) and (string.sub(s, slen-2) == "\r\n>") then
			result = true
		elseif (slen >= 1) and (string.sub(s, slen) == ">") then
			result = true
		end
	end

	SMC:Log("debug", "Acked("..tostring(s).."): "..tostring(result))
	return result

end

function AppInit0()

	if port == nil then
		port = SMC:NewSerialPort("config1")
	end

end

function AppInit()

	gasynch = SMCContext
	gasynch:SetAsynch()

	AppInit0()

end

function DoOnline()

	SMC:Log("debug", "DoOnline")
	Send("ATO\r")

	local rxd = AwaitResponse(30000)
	local result = false

	if (rxd ~= nil) and string.find(rxd, "CONNECT [0-9]*") then
		result = true
	end

	SMC:Log("debug", "DoOnline: "..tostring(result))
	return result

end

function DoPower(newState)

	if newState == 0 then
		part1="Dis"
	else
		part1="En"
	end

	SMC:Log("debug", part1.."abling the power");
	
	local executeResult = os.execute("sudo SetPowerSwitch 3 "..tostring(newState))
	
	if executeResult ~= 0 then
		SMC:Log("error", "--> exit code is "..tostring(executeResult))
	end

	return executeResult == 0;

end

function DoPowerOff()

	local result = DoPower(0)

	if result then
		Wait(20000)
	end

	return result

end

function DoPowerOn()

	local result = DoPower(1)

	if result then

		local rxd = "Not nil"
		local found = false

		result = false

		while rxd ~= nil and not found do

			rxd = AwaitData(60000)

			if rxd ~= nil then
				if Acked(rxd) then
					found = true
					result = true
				elseif string.find(rxd, "CONNECT") ~= nil then
					rxd = AwaitData(3000)
					found = true
					result = DoTestComms()
				end
			else
				result = DoTestComms()
			end
		end
		
	end

-- Send("ATI\r")
-- AwaitData(5000)
-- Send("ATS?\r")
-- AwaitData(5000)

	SMC:Log("debug", "DoPowerOn: "..tostring(result))
	return result

end

function DoTestComms()

	local result = false
	local rxd, rxd2

	Send("++++")
	rxd = AwaitData(1000)
	Send("\r")
	rxd2 = AwaitData(1000)

	if Acked(rxd) or Acked(rxd2) then
		result = true
	end

	SMC:Log("debug", "DoTestComms: "..tostring(result))
	return result

end

function Escape(s)

	local result

	if s == nil then
		result = "nil"
	else
		result = "'"..string.gsub(string.gsub(string.gsub(s,"\\","\\\\"),"\r","\\r"),"\n","\\n").."'"
	end

	return result

end

function PowerOff()

	AppInit()

	appRtn = coroutine.create(function()
		DoPowerOff()
		gasynch:ReportComplete()
		gasynch = nil
	end)

	coroutine.resume(appRtn)

end

function PowerOn()

	AppInit()

	appRtn = coroutine.create(function()
		DoPowerOn()
		gasynch:ReportComplete()
		gasynch = nil
	end)

	coroutine.resume(appRtn)

end

function lines(str)
  local t = {}
  local function helper(line) table.insert(t, line) return "" end
  helper((str:gsub("(.-)\r?\n", helper)))
  return t
end

function DoListen1Inner()

	SMC:Log("debug", "DoListen1Inner")
	local good = true
	local msg = ""
	local result = ""

	if not DoTestComms() then
		good = DoPowerOn()
		if not good then
			msg = "DoPowerOn failed"
		end
	else
		msg = "DoTestComm failed"
	end

	if not good then
		result = msg
	elseif not DoOnline() then
		result = "DoOnline failed"
	else
		--SMC:Log("debug", "DoListen1 starting wait")
		local accdata = ""
		local isdone = false
		--SMC:Log("debug", "here")
		local startTime = SMC:SystemUptime()
		--SMC:Log("debug", "here2")
		local timeout = 600

		--SMC:Log("debug", "startTime="..tostring(startTime))

		while not isdone
		do
			
			local elapsed = SMC:SystemUptime()-startTime
			local remaining = math.max(0, timeout-elapsed)

			SMC:Log("debug", "#accdata="..tostring(#accdata).." elapsed="..tostring(elapsed).." remaining="..tostring(remaining))

			if remaining > 0 then
			
				local rxd = AwaitData(1000*remaining)

				if #rxd == 0 then
					isdone = true
					result = "Nothing received or timeout after "..tostring(#accdata).." bytes"
				else
					local t = lines(rxd)

					SMC:Log("debug", tostring(#t).." lines")

					for index,value in ipairs(t) do
						local rxtime = string.find(value, "Rx[ ]Time")
						local crc = string.find(value, "CRC")
						if rxtime or crc then
							if #accdata  ~= 0 then
								accdata = accdata..","..value
							else
								accdata = value
							end
							if crc then
								isdone = true
								result = Escape(accdata)
							end
						end
					end
				end

			else
				isdone = true
				result = "Timeout after "..tostring(#accdata).." bytes"
			end

		end
	end

	SMC:Log("debug", "DoListen1Inner: "..result)
	return result

end

function DoListen1()

	SMC:Log("debug", "DoListen1")
	gasynch:AppendOutput(DoListen1Inner())
	gasynch:ReportComplete()
	gasynch = nil
	SMC:Log("debug", "DoListen1 done")

end

function Listen1()

	AppInit()
	appRtn = coroutine.create(function() DoListen1() end)
	coroutine.resume(appRtn)
	SMC:Log("debug", "exiting from Listen1 outer")

end

function ReportStringAsTelemetry(str)

	local evt = { }
	
	evt.header = { name = "LWTelem1", typename = "LWTelem1", domainname="LqrSMAC" }
	evt.properties = { }
	evt.properties["Telem-BoardID"] = { 32, "UINT8" }
	evt.properties["Telem-TaskID"] = { 64, "UINT8" }
	evt.properties["StringMessage"] = str
	
	SMC:PublishEvent(evt)

end

function ListenLoop()

	appRtn = coroutine.create(function() AppInit0() while true do ReportStringAsTelemetry(DoListen1Inner()) Wait(5000) end end)
	coroutine.resume(appRtn)

end

function AwaitData(timeout)

	SMC:Log("debug", "AwaitData timeout="..timeout)

	local rxd

	awaitTimer = SMC:NewTimer("await", timeout)
	rxd = coroutine.yield()
	awaitTimer = nil
	collectgarbage("collect")

	SMC:Log("debug", "AwaitData:"..Escape(rxd))
	return rxd

end

function AwaitResponse(timeout)

	SMC:Log("debug", "AwaitResponse timeout="..timeout)

	local rxdTotal, rxd, busy

	awaitTimer = SMC:NewTimer("await", timeout)
	busy = true
	rxdTotal = ""

	while (awaitTimer ~= nil) and busy do

		rxd = coroutine.yield()

		if rxd ~= nil then
			rxdTotal = rxdTotal..rxd
		end

		if Acked(rxdTotal) then
			SMC:Log("debug", "AwaitResponse: not busy because total was acked")
			busy = false
		end

	end

	awaitTimer = nil
	collectgarbage("collect")
	SMC:Log("debug", "AwaitResponse:"..Escape(rxdTotal))
	return rxdTotal

end

function Send(msg)

	SMC:Log("debug", "Sending "..Escape(msg))
	port:Transmit(msg)

end

function DoSendCmd(cmdname)

	SMC:Log("debug", "DoSendCmd "..Escape(cmdname))

	if not DoTestComms() then
		DoPowerOn()
	end

	Send(cmdname.."\r")

	local response = AwaitResponse(60000)

	gasynch:AppendOutput(Escape(response))
	gasynch:ReportComplete()
	gasynch = nil
		
end

function SendCmd(cmdname)

	AppInit()
	appRtn = coroutine.create(function() DoSendCmd(cmdname) end)
	coroutine.resume(appRtn)

end

function Wait(timeout)

	waitTimer = SMC:NewTimer("wait", timeout)

	while waitTimer ~= nil
	do
		coroutine.yield()
	end 

end

function SMCReportSerialData(portName,data)

	if (data ~= nil) and (string.len(data) ~= 0) then

		SMC:Log("debug", "Received "..tostring(#data).." bytes:"..Escape(data).." on "..portName)

		if awaitTimer ~= nil then
			--awaitTimer = nil
			--collectgarbage("collect")
			coroutine.resume(appRtn, data)
		end

	end

end

function SMCReportTimeout(timerName)

	SMC:Log("debug", "SMCReportTimeout timerName="..timerName)

	if timerName == "wait" then
		waitTimer = nil
		collectgarbage("collect")
		coroutine.resume(appRtn, nil)
	elseif timerName == "await" then
		awaitTimer = nil
		collectgarbage("collect")
		coroutine.resume(appRtn, nil)
	end

	SMC:Log("debug", "exiting SMCReportTimeout")

end

gasynch = nil
appRtn = nil

