#!/opt/lnxall_app/bin/lua

-- Created by jiaqiang.ye@lnxall.com
-- Network 4G service for Debian-based gateways
-- 2023/12/01

-- Load external modules
local uci = require 'uci'
local bit32 = require 'bit32'
local posix = require 'posix'
local ulog = require 'ubuslog'
local invoker = require 'invoker'
local modem4g = require 'modem4g'

local g_modem = nil -- from modem4g.getmodem
local plat4gOps = {} -- Platform dependent 4G operations
-- global CONSTANT variables
local g_4gtty = "/tmp/4g_modem_tty"
local g_netuci = "/opt/lnxall_app/bin/netuci"
local g_recount = 0
local gfmt = string.format
local CHECK_INTERVAL = 180 -- check-interval for 4G network connection
local PING_ADDRS = { [1] = 'emsmqtt.lnxall.com', [2] = 'smc.lnxall.com',
	[3] = 'emsiot.lnxall.com', [4] = 'mqtt.lnxall.com' }

local g_metrics, g_metric_idx = { 100, 50, 0 }, 1
local g_resolved = {} -- host to IPv4 address mapping
local g_backcfg = "/opt/lnxall_app/platspec/etc-config-network"

-- global varibales
local g_usbid = nil   -- USB ID of 4G network modem
local g_netdev = nil  -- name of USB network device
local g_4ginfo = {}   -- Array of supported USB modem
local g_dhcp = {}
local g_dns = { [1] = '8.8.8.8', [2] = '114.114.114.114' }

g_4ginfo['2c7c:6005'] = { ['ttyindex'] = 2, ['script'] = 'ec200.gcom', -- EC200A
	['bringup'] = 'AT+QCFG="usbnet",1\r\n', }
g_4ginfo['2c7c:6002'] = { ['ttyindex'] = 1, ['script'] = 'ec200.gcom', -- EC200N
	['bringup'] = 'AT+QCFG="usbnet",1\r\n', }
g_4ginfo['2c7c:0904'] = { ['ttyindex'] = 0, ['script'] = 'ec800g.gcom', -- EC800G
	['bringup'] = 'AT+QCFG="usbnet",1\r\n', }
g_4ginfo['2c7c:6026'] = { ['ttyindex'] = 2, ['script'] = 'ec200.gcom' } -- EC200T
g_4ginfo['2c7c:0901'] = { ['ttyindex'] = 0, ['script'] = 'ec200u.gcom' } -- EC200U
g_4ginfo['2c7c:0125'] = { ['ttyindex'] = 2, ['script'] = 'ec2x.gcom' } -- 全球通模组
g_4ginfo['1782:4e00'] = { ['ttyindex'] = 1, ['script'] = 'air720.gcom',
	['bringup'] = "AT+SETUSB=2\r\n", } -- Air724UG
g_4ginfo['19d1:0001'] = { ['ttyindex'] = 0, ['script'] = 'air780.gcom'} --Air780.gcom
g_4ginfo['2cb7:0a0a'] = { ['ttyindex'] = 0, ['script'] = 'mc661.gcom'} -- MC661
g_4ginfo['2cb7:0a0b'] = { ['ttyindex'] = 0, ['script'] = 'mc661.gcom'} -- MC661
g_4ginfo['2cb7:0a0c'] = { ['ttyindex'] = 0, ['script'] = 'mc661.gcom'} -- MC661

local function netdev_exists(iface)
	if type(iface) ~= "string" or #iface == 0 then
		return false -- invalid method argument
	end
	local rval = invoker.invoke(invoker.NOSTDIO,
		"ip", "link", "show", "dev", iface)
	return rval == 0
end

local function nslookup(hostn)
	local pid, pfd = invoker.invoke(invoker.NOWAIT + invoker.NOSTDIO + invoker.OUTPUT, "nslookup", hostn)
	if not pid then return nil end

	local swait = invoker.uptimsec()
	local nwait, exited = swait, false
	swait = swait + 4000
	while nwait < swait do
		if invoker.waitpid(pid, true) then
			exited = true
			break
		end
		invoker.msleep(250)
		nwait = invoker.uptimsec()
	end

	if not exited then
		invoker.kill(pid, 9)
		invoker.waitpid(pid, false)
	end

	local output = invoker.readfd(pfd, 8192)
	if type(pfd) == "number" then
		invoker.closefd(pfd); pfd = nil
	end
	if type(output) ~= "string" then return nil end

	local iptab, ipn, ans = {}, 0, false
	for line in string.gmatch(output, "[^\r\n]+") do
		if not ans then
			if string.find(line, "answer:", 1, true) then ans = true end
		else
			local ipv4 = string.match(line, "Address:%s+([%d%.]+)")
			if ipv4 and invoker.isipv4(ipv4) then
				ipn = ipn + 1
				iptab[ipn] = ipv4
			end
		end
	end

	if ipn == 0 then return nil end
	return iptab
end

local function has_ipv4(ndev)
	local okay, output = invoker.invoke(invoker.OUTPUT, "ip", "addr", "show", "dev", ndev)
	if okay ~= 0 or type(output) ~= "string" then return false end

	local ipaddr = string.match(output, "inet%s+([%d%.]+)/")
	if not ipaddr then return false end
	return ipaddr
end

local function find_4g_netdev()
	local okay, output = invoker.invoke(invoker.OUTPUT, "ip", "link", "show")
	if okay ~= 0 or type(output) ~= "string" then
		return nil
	end
	for mdev in string.gmatch(output, "enp[^:\r\n]+:") do
		local nlen = string.len(mdev)
		if nlen >= 9 then return mdev:sub(1, nlen - 1) end
	end
	return nil
end

local function network_disabled(which)
	if type(which) ~= "string" then which = 'wwan' end
	local nuci = uci.cursor('/etc/config')
	local iface = nuci:get('network', which)
	if iface == nil then
		-- no configuration found. default to true
		nuci:close(); nuci = nil
		return false
	end

	local dev = nuci:get('network', which, 'device')
	local dis = nuci:get('network', which, 'disabled')
	nuci:close(); nuci = nil
	if dev == nil or dis == "1" or dis == 1 then return true end
	return false
end

local function get_vid_pid(device_path) -- get the Vendor and Product ID from the real device path
    local vid, pid
    local path = device_path
    repeat
        local uevent_file = io.open(path .. "/uevent")  -- make sure the V and P id is saved in uevent, of course uedvadm also will get the information from uevent file
        if uevent_file then
            for line in uevent_file:lines() do
                local key, value = line:match("(%a+)=(.+)")
                if key == "PRODUCT" then
                    vid, pid = value:match("(%x+)%/(%x+)%/(%x+)")
                    if vid and pid then
                        vid = string.format("%04x", tonumber(vid, 16))
                        pid = string.format("%04x", tonumber(pid, 16))
                        return vid .. ":" .. pid
                    end
                end
            end
            uevent_file:close()
        end
        path = path:match("(.+)/[^/]+") -- if not found, next step is to check the parent directory. 
    until not path or path == ""
    return nil, nil
end

local function find_device_path(device_name)
    local device_path
    -- unfortunately, this method is strong depends on kernel version. As far as I know it can't work on 2.4 kernel.
    for class in io.popen("ls " .. "/sys/class/"):lines() do
        local path = "/sys/class/" .. class .. "/" .. device_name

        if io.open(path) then
            local link = io.popen("readlink -f " .. path):read("*line") --  get the link path
            if link then
                while link:match("%.%./") do
                    link = link:gsub("/[^/]+/%.%./", "/") -- get the real path
                end
                device_path = link
                break
            end
        end
    end
    return device_path
end

local function fetch_ttyusb(idx)
	local jdx, kdx = 0, 0
	while jdx < 40 do
		local ndev = gfmt("/sys/bus/usb-serial/drivers/option1/ttyUSB%d", jdx)
		if posix.access(ndev) == 0 then
			ndev = gfmt("/dev/ttyUSB%d", jdx)
			local info = posix.stat(ndev)
			if type(info) == "table" and info["type"] == "character device" then
				if kdx >= idx then return ndev end
				kdx = kdx + 1
			end
		end
		jdx = jdx + 1
	end
	jdx = 0
	kdx = 0
	while jdx < 40 do
		local ndev = find_device_path (gfmt ("ttyACM%d", jdx))
		if not ndev then break end
		local vpid = get_vid_pid (ndev)
		if vpid == g_usbid then
			if posix.access(ndev) == 0 then
				ndev = gfmt("/dev/ttyACM%d", jdx)
				local info = posix.stat(ndev)
				if type(info) == "table" and info["type"] == "character device" then
					if kdx >= idx then return ndev end
					kdx = kdx + 1
				end
			end
		end
		jdx = jdx + 1
	end
	return nil
end

local function update_sim_iccid(usbdev)
	local outfile = modem4g.ICCID_FILE
	if posix.access(outfile) == 0 then return true end
	if not usbdev then
		if not g_usbid then return false end
		local i4g = g_4ginfo[g_usbid]
		usbdev = fetch_ttyusb(i4g.ttyindex)
	end
	if posix.access(usbdev) ~= 0 then return false end

	invoker.invoke(invoker.NOSTDIO, "stty", "-F", usbdev, "raw", "cs8", "-echo")
	local usbfd = posix.open(usbdev, posix.O_RDWR + posix.O_NONBLOCK + posix.O_NOCTTY)
	if type(usbfd) ~= "number" or usbfd < 0 then return false end
	while true do
		local ldat = posix.read(usbfd, 1024)
		if type(ldat) ~= "string" or #ldat == 0 then break end
	end

	local restab = {}
	posix.write(usbfd, "AT\r\n"); invoker.msleep(400)
	posix.write(usbfd, "AT+QCFG=\"nat\"\r\n"); invoker.msleep(400)
	posix.write(usbfd, "AT+ICCID\r\n"); invoker.msleep(400)
	posix.write(usbfd, "AT+QICCID\r\n"); invoker.msleep(400)
	posix.write(usbfd, "AT+CCID\r\n"); invoker.msleep(400)
	while true do
		local ldat = posix.read(usbfd, 1024)
		if type(ldat) == "string" and #ldat > 0 then
			restab[#restab + 1] = ldat
		else
			break
		end
	end
	local output = #restab > 0 and table.concat(restab) or nil
	if type(output) ~= "string" then
		posix.close(usbfd)
		return false
	end
	local iccid = string.match(output, "ICCID:%s+([%d%a]+)")
	if iccid == nil then iccid = string.match(output, "CCID:%s+([%d%a]+)") end
	if iccid then
		local filp = io.open(outfile, "wb")
		if filp then
			filp:write(gfmt("ICCID: %s\n", iccid))
			filp:close(); filp = nil
		end
	end

	-- check for nat configuration
	if string.find(output, 'QCFG: "nat"', 1, true) and
		not string.find(output, 'QCFG: "nat",1', 1, true) then
		posix.write(usbfd, "AT+QCFG=\"nat\",1\r\n"); invoker.msleep(300)
		posix.write(usbfd, "AT+CFUN=1,1\r\n"); invoker.msleep(1500)
	end

	posix.close(usbfd)
	return iccid
end

local function get_resolv()
	local dhcp = {}
	local del = { [g_dns[1]] = true, [g_dns[2]] = true, ["127.0.0.53"] = true }
	local file = io.open("/etc/resolv.conf", "r")
	if not file then return false end

	local str = file:read("*all")
	if type(str) ~= "string" or string.find(str,"deb4gnet.lua") or string.find(str,"DNSWATCHER") then
		file:close()
		return false
	end
	
	g_dhcp = {}
	for line in string.gmatch(str, "[^\r\n]+") do
		if line ~= "" and not line:match("^#") then
			local ip = line:match("^nameserver%s+([%d%.]+)")
			if ip and invoker.isipv4(ip) then dhcp[#dhcp + 1] = ip end
		end
	end
	file:close()

	for i = 1, #dhcp do
		if not del[dhcp[i]] then
			g_dhcp[#g_dhcp + 1] = dhcp[i]
			del[dhcp[i]] = true
		end
	end
	return true
end

local function check_resolv()
	local has_sr = ""
	if posix.access('/lib/systemd/systemd-resolved') == 0 then
		local conts, restart = {}, false
		local syscfg = '/etc/systemd/resolved.conf'

		conts[#conts + 1] = "# Generated by deb4gnet.lua\n[Resolve]\n"
		conts[#conts + 1] = string.format("DNS=%s %s\n", g_dns[1], g_dns[2])
		conts[#conts + 1] = "DNSSEC=no\n"
		conts[#conts + 1] = "Cache=yes\n"
		conts[#conts + 1] = "LLMNR=false\n"
		conts = table.concat(conts)

		local sfile, sdat = io.open(syscfg, "rb"), nil
		if sfile then
			sdat = sfile:read("*a")
			sfile:close(); sfile = nil
		end

		if sdat ~= conts then
			restart = true
			sfile = io.open(syscfg, "wb")
			if sfile then
				sfile:write(conts)
				sfile:close(); sfile = nil
			end
		end

		if not restart then
			local _, output = invoker.invoke(invoker.OUTPUT, "systemctl", "status", "systemd-resolved")
			if type(output) ~= "string" then output = "" end
			if not string.find(output, "Active: active (running)", 1, true) then
				restart = true
			end
		end

		if restart then
			io.stdout:write("Restarting systemd-resolved service...\n")
			io.stdout:flush()
			invoker.invoke(0, "systemctl", "enable", "systemd-resolved")
			invoker.invoke(0, "systemctl", "restart", "systemd-resolved")
		end
		has_sr = "nameserver 127.0.0.53\n"
	end

	get_resolv()
	local content = gfmt("# Generated by deb4gnet.lua\n%s",has_sr)
	for i = 1, #g_dhcp do
		content = content .. gfmt("nameserver %s\n",g_dhcp[i])
	end
	content = content .. gfmt("nameserver %s\nnameserver %s\noptions timeout:12 attempts:3\n",g_dns[1], g_dns[2])

	local rfile = "/etc/resolv.conf"
	local resolv, cont = io.open(rfile, "rb"), nil
	if resolv then
		cont = resolv:read('*a')
		resolv:close(); resolv = nil
	end
	if cont == content then return true end
	-- if cont then io.stdout:write("Warning: old DNS settings:\n" .. cont) end
	ulog.log(ulog.LOG_INFO, "Network4g: updating resolv.conf...")
	resolv = io.open(rfile, "wb")
	if resolv then
		resolv:write(content)
		resolv:close(); resolv = nil
		posix.sync()
		return true
	end
	return false
end

local function systemd_restart(initp, name)
	if posix.access(initp) == 0 then
		return invoker.invoke(invoker.NOSTDIO, "systemctl", "restart", name) == 0
	end
	return false
end

local function wwan_cannot_up()
	local usbdev = invoker.readfile(g_4gtty, invoker.TRIMEND)
	if type(usbdev) ~= "string" then usbdev = "/dev/XXX" end
	local usbfd = posix.open(usbdev, bit32.bor(posix.O_RDWR, posix.O_NONBLOCK, posix.O_NOCTTY))
	if type(usbfd) ~= "number" or usbfd < 0 then
		io.stderr:write(gfmt("failed to open %s for restart NetworkManager\n", usbdev))
		io.stderr:flush()
		return false
	end

    posix.write(usbfd, "AT+CEREG?\r\n"); invoker.msleep(400)
    local ldat = posix.read(usbfd,1024)
    if type(ldat) ~= "string" or #ldat <= 0 or not string.match(ldat,"0,1") then
		posix.close(usbfd)
		return false
	end

    posix.write(usbfd, "AT+CPIN?\r\n"); invoker.msleep(400)
    ldat = posix.read(usbfd,1024)
    if type(ldat) ~= "string" or #ldat <= 0 or not string.match(ldat,"READY") then
		posix.close(usbfd)
		return false
	end
	posix.close(usbfd)

	if invoker.invoke(0, "nmcli", "-w", "6", "con", "up", "wwan") == 0 then
		return false
	end
    return true
end

local function net4g_reload(iface, metric)
	local okay, output = invoker.invoke(invoker.OUTPUT,
		"nmcli", "-m", "tabular", "-t", "con", "show")
	if okay ~= 0 or type(output) ~= "string" then output = "" end

	local ndels = 0
	for line in string.gmatch(output, "[^\r\n]+") do
		local con = string.match(line, "^([^:]+)")
		if con and con ~= 'wwan' then
			local cinfo = nil
			okay, cinfo = invoker.invoke(invoker.OUTPUT + 0x70000,
				"nmcli", "con", "show", con)
			if okay == 0 and type(cinfo) == "string" and string.find(cinfo, iface, 1, true) then
				ndels = ndels + 1
				invoker.invoke(invoker.NOSTDIO, "nmcli", "con", "down", con)
				invoker.invoke(invoker.NOSTDIO, "nmcli", "con", "delete", con)
				io.stderr:write(gfmt("Delete NetworkManager connection: %s\n", con))
				io.stderr:flush()
			end
		end
	end

	local wuci, doc = uci.cursor('/etc/config'), false
	local ndev = wuci:get('network', 'wwan', 'device')
	if metric then
		metric = tostring(metric)
		local oldm = wuci:get('network', 'wwan', 'metric')
		if metric ~= oldm then
			if type(oldm) ~= "string" then oldm = "nil" end
			io.stdout:write(gfmt("Update wwan metric from %s to %s\n",
				oldm, metric))
			io.stdout:flush()

			doc = true
			wuci:delete('network', 'wwan', 'metric')
			wuci:delete('network', 'wwan', 'metric')
			wuci:set('network', 'wwan', 'metric', metric)
		end
	end
	if ndev ~= iface then
		if ndev == nil then
			-- recheck whether wwan section exists
			ndev = wuci:get('network', 'wwan')
		end
		if ndev == nil then
			-- configuration section might be missing, force copy of network-configuration
			local r = invoker.invoke(invoker.NOSTDIO, "cp", "-v", "-f", g_backcfg, "/etc/config/network")
			if type(r) ~= "number" then r = 10 end
			io.stderr:write(gfmt("Error, restoring network configuration from '%s': %d\n", g_backcfg, r))
			io.stderr:flush()
		else
			doc = true
			wuci:delete('network', 'wwan', 'device')
			wuci:delete('network', 'wwan', 'device')
			wuci:set('network', 'wwan', 'device', iface)
		end
	end
	if doc then wuci:commit('network') end
	invoker.invoke(invoker.NOSTDIO, "lua", g_netuci)
	invoker.msleep(5000)
	if wwan_cannot_up() then
		io.stdout:write("restart NetworkManager")
		io.stdout:flush()
		invoker.invoke(invoker.NOSTDIO, "systemctl", "restart", "NetworkManager")
		invoker.msleep(3000)
		systemd_restart("/etc/systemd/system/multi-user.target.wants/bdhcpd.service", "bdhcpd")
		systemd_restart("/etc/systemd/system/multi-user.target.wants/lnxhostapd.service", "lnxhostapd")
	end
	invoker.invoke(0, "nothup", "/bin/sh", "-c", gfmt("sleep 30 && exec lua %s", g_netuci))
	return true
end

local function network_connect(iface)
	-- kill dhclient application first
	local i4g = g_4ginfo[g_usbid]

	local dftdev = gfmt('/dev/ttyUSB%d', i4g.ttyindex)
	-- check for /dev/ttyUSBx device
	local devn = fetch_ttyusb(i4g.ttyindex) or dftdev
	update_sim_iccid(devn)
	local devi = posix.stat(devn)
	if type(devi) ~= "table" or devi["type"] ~= "character device" then
		io.stderr:write(gfmt("Error, ttyUSB device not found: %s\n", devn))
		io.stderr:flush()
		return false
	end

	-- update `4g_modem_tty unconditionally
	local htty = io.open(g_4gtty, "wb")
	if htty then
		htty:write(devn); htty:write("\n")
		htty:close()
		htty = nil
	end

	-- miscellaneous operations for 4G modem
	if g_modem then g_modem.update(g_modem, devn, false) end

	if i4g.script then -- we need to execute comgt script
		invoker.invoke(invoker.NOSTDIO, "ip", "addr", "flush", iface)
		-- invoke comgt to establish network connection
		local okay = invoker.invoke(invoker.NOSTDIO, 'comgt', '-d', devn, '-s',
			gfmt('/opt/lnxall_app/etc/gcom/%s', i4g.script))
		if okay ~= 0 then
			io.stderr:write(gfmt("Error, comgt has returned %d\n",
				type(okay) == "number" and okay or 1))
			io.stderr:flush()
		end
	elseif i4g.bringup then
		local tfd = posix.open(devn, posix.O_RDWR + posix.O_NONBLOCK + posix.O_NOCTTY)
		if type(tfd) == "number" and tfd >= 0 then
			posix.write(tfd, i4g['bringup'])
			posix.close(tfd)
		end
	end

	local count = 0
	invoker.msleep(5000) -- delay 3 seconds before continue
	-- wait for `iface network to show up
	while true do
		count = count + 1
		if count >= 10 then break end
		if netdev_exists(iface) then break end
		invoker.msleep(1000)
	end

	invoker.msleep(1000) -- again, delay one second
	-- check again for the existence of `iface
	if not netdev_exists(iface) then
		io.stderr:write(gfmt("Error, network device not found: %s\n", iface))
		io.stderr:flush()
		return false
	end

	local midx = nil
	if invoker.uptime() >= 90 then
		midx = g_metric_idx
		midx = midx + 1
		if midx > 3 then midx = 1 end
		g_metric_idx = midx
		midx = g_metrics[midx]
	end

	net4g_reload(iface, midx)
	return true
end

-- initialize working environment
local function n4g_init(initcnt)
	-- Enumerate output from `lsusb,
	-- and determine the 4G modem USB ID
	local okay, outbuf = invoker.invoke(invoker.OUTPUT, "lsusb")
	if okay ~= 0 or type(outbuf) ~= "string" or string.len(outbuf) == 0 then
		io.stderr:write("Error, failed to invoke `lsusb!\n")
		io.stderr:flush()
		return false
	end

	-- process line by line
	for output in string.gmatch(outbuf, "([^\r\n]+)") do
		local usbid = string.match(output, "(%x%x%x%x:%x%x%x%x)")
		if usbid and g_4ginfo[usbid] then
			g_usbid = usbid
			break
		end
	end

	if not g_usbid then
		if (initcnt % 32) == 0 then
			io.stderr:write("Warning, no 4G network modem found!\n")
			io.stderr:flush()
		end
		return false
	end

	-- write USBID to `/sys/bus/usb-serial/drivers/option1/new_id
	local opt = string.gsub(g_usbid, ":", " ") .. " ff\n"
	local new_id = "/sys/bus/usb-serial/drivers/option1/new_id"
	local oldpt = invoker.readfile(new_id)
	if opt ~= oldpt then
		local opth = io.open(new_id, "wb")
		if opth then opth:write(opt); opth:close(); opth = nil end
		invoker.waitsec(4)
	end
	opt, new_id, oldpt = nil, nil, nil -- remove local variables

	local nowtim = invoker.uptime()
	while nowtim < 65 do -- do not check 4G network in such a haste
		check_resolv() -- update resolv.conf before wait
		invoker.waitsec(65 - nowtim)
		nowtim = invoker.uptime()
	end
	check_resolv()

	local ttydev = fetch_ttyusb(g_4ginfo[g_usbid]["ttyindex"])
	if ttydev then
		-- write /tmp/4g_modem_tty with the control path of ttyUSB device
		local fhdl = io.open(g_4gtty, "wb")
		if fhdl then
			fhdl:write(gfmt("%s\n", ttydev))
			fhdl:close(); fhdl = nil
		end
	end

	-- check network device
	g_netdev = plat4gOps.netdev()
	if type(g_netdev) ~= "string" or not netdev_exists(g_netdev) then
		g_netdev = nil
		local info4g = g_4ginfo[g_usbid]
		if ttydev and info4g['bringup'] then
			local tfd = posix.open(ttydev, posix.O_RDWR + posix.O_NONBLOCK + posix.O_NOCTTY)
			if type(tfd) == "number" and tfd >= 0 then
				posix.write(tfd, info4g['bringup'])
				posix.close(tfd)
			end
		end
		g_modem = modem4g.getmodem(g_usbid)
		if g_modem then g_modem.init(g_modem, ttydev) end
		if (initcnt % 0x5) == 0 then
			plat4gOps.poweron(false)
		elseif (initcnt % 0x5) == 1 then
			plat4gOps.poweron(true)
		elseif (initcnt % 0x5) == 2 then
			network_connect('wwan0')
		end
		io.stderr:write("Error, cannot determine 4G network device.\n")
		io.stderr:flush()
		return false
	end
	g_modem = modem4g.getmodem(g_usbid)
	if g_modem then g_modem.init(g_modem, ttydev) end

	local fc = '/etc/force_connect.txt'
	if invoker.uptime() < 240 and posix.access(fc) ~= 0 then
		network_connect(g_netdev)
		local filp = io.open(fc, "wb")
		if filp then
			filp:write(gfmt("%s: Forcing network reconnect.\n", os.date()))
			filp:close(); filp = nil
		end
		invoker.waitsec(15)
	elseif not has_ipv4(g_netdev) then
		invoker.invoke(invoker.NOSTDIO, "nmcli", "con", "del", "wwan")
		invoker.invoke(invoker.NOSTDIO, "lua", g_netuci)
	end
	return true
end

local function network_okay(iface)
	local cmds = { "ping", "-c3", "-w12" }
	local cmdidx = 0x1 + #cmds
	if type(iface) == "string" then
		cmds[cmdidx] = "-I"
		cmds[cmdidx + 0x1] = iface
		cmdidx = cmdidx + 0x2
	end
	for _, hname in pairs(PING_ADDRS) do
		local hipv4 = nil
		if invoker.isipv4(hname) then
			hipv4 = hname
		else
			hipv4 = g_resolved[hname]
			if not hipv4 then
				local ips = nslookup(hname)
				if ips then
					hipv4 = ips[1]
					io.stdout:write(gfmt("DNS resolved: %s => %s\n", hname, hipv4))
					io.stdout:flush()
					g_resolved[hname] = hipv4
				end
			end
		end
		if hipv4 then
			cmds[cmdidx] = hipv4
			local okay, output = invoker.invoke(invoker.OUTPUT + invoker.NOSTDIO, cmds)
			if type(output) == "string" and
				string.find(output, 'packet loss', 1, true) and
				not string.find(output, '100% packet loss', 1, true) then
				return hname
			end
			if okay == 0 then return hname end
			if invoker.tcpcheck(hipv4, 80, 1500) then return hname end
			if invoker.tcpcheck(hipv4, 3883, 1500) then return hname end
		end
	end
	return false
end

local function reset_4g_modem(sdely)
	local resettag = '/tmp/.4g_modem_reset'
	if posix.access(resettag) == 0 then return false end

	local i4g = g_4ginfo[g_usbid]
	local dftdev = gfmt('/dev/ttyUSB%d', i4g.ttyindex)
	local devn = fetch_ttyusb(i4g.ttyindex) or dftdev
	local devi = posix.stat(devn)
	if type(devi) ~= "table" or devi["type"] ~= "character device" then
		io.stderr:write(gfmt("Error, ttyUSB device not found: %s\n", devn))
		io.stderr:flush()
		return false
	end

	local usbfd = posix.open(devn, bit32.bor(posix.O_WRONLY, posix.O_TRUNC,
		posix.O_NONBLOCK, posix.O_NOCTTY))
	if type(usbfd) ~= "number" or usbfd < 0 then
		io.stderr:write(gfmt("Error, failed to open %s for reset\n", devn))
		io.stderr:flush()
		return false
	end

	posix.write(usbfd, "AT+RESET\r\n"); invoker.msleep(300); posix.close(usbfd)
	local tagf = io.open(resettag, "wb")
	if tagf then
		tagf:write(gfmt("%s: 4G modem reset via %s\n", os.date(), devn))
		tagf:close(); tagf = nil
	end
	invoker.waitsec(sdelay)
	return true
end

local function watch_resolv(delay)
	local flags = bit32.bor(invoker.IN_CREATE, invoker.IN_DELETE_SELF,
		invoker.IN_MODIFY, invoker.IN_CLOSE_WRITE)
	local rfile = '/etc/resolv.conf'
	rfile = invoker.realpath(rfile)
	if not posix.access(rfile) then check_resolv() end

	local changed, errmsg = invoker.watchfile(flags, delay * 1000, nil, rfile)
	if changed == nil then
		if type(errmsg) ~= "string" then errmsg = "unknown watchfile error" end
		io.stderr:write(errmsg); io.stderr:write("\n"); io.stderr:flush()
		invoker.waitsec(delay)
		return false
	end
	if changed and changed > 0 then
		invoker.msleep(1500) -- delay 1.5 second to avoid modifying by too many processes
		check_resolv()
	end
	return true
end

local function isat_time(hour)
	local nowt = os.date("*t")
	return nowt["hour"] == hour
end

local function mainloop()
	local dcnt = 0 -- disconnect count
	local nowtim = invoker.uptime()
	local nextim = nowtim + CHECK_INTERVAL
	update_sim_iccid()
	while true do
		check_resolv() -- check resolv.conf before continue
		local phost = network_okay()
		if phost then
			io.stdout:write(gfmt("%s: Network ping okay: %s\n", os.date(), phost))
			io.stdout:flush()
			if dcnt > 0 then dcnt = 0 end
			if posix.access(modem4g.ICCID_FILE) ~= 0 then update_sim_iccid() end
			if g_modem then g_modem.update(g_modem, nil, true) end
		elseif not network_disabled() then
			if isat_time(23) and dcnt >= 4 and posix.access(modem4g.ICCID_FILE) == 0 then -- it is 23 at midnight, safe to reboot
				ulog.log(ulog.LOG_ERR, gfmt("reboot due to 4G network failure: %d.", dcnt))
				invoker.invoke(invoker.NOSTDIO, "reboot"); invoker.waitsec(5)
			end

			if not reset_4g_modem(10) and bit32.band(dcnt, 0x1) == 0x1 then
				plat4gOps.poweron(false); invoker.waitsec(3)
				plat4gOps.poweron(true); invoker.waitsec(12)
			end
			dcnt = dcnt + 1 -- increment disconnected

			g_netdev = plat4gOps.netdev()
			if not netdev_exists(g_netdev) then
				ulog.log(ulog.LOG_ERR, gfmt("Error, no 4G net device found: %d", dcnt))
			else
				local midx = math.floor(dcnt % 2) + 1
				ulog.log(ulog.LOG_ERR, gfmt("reconnect 4G network, count: %d", dcnt))
				network_connect(g_netdev)
			end
		end

		check_resolv() -- check resolv.conf again
		nowtim = invoker.uptime()
		-- delay until next checking time
		while nowtim < nextim do
			watch_resolv(nextim - nowtim)
			nowtim = invoker.uptime()
		end
		-- increment next checking time
		nextim = nextim + CHECK_INTERVAL
		if nextim <= nowtim then nextim = nextim + CHECK_INTERVAL end
	end
end

local function check_platform()
	local bname = invoker.readfile('/tmp/sysinfo/board_name', invoker.TRIMEND)
	if bname == 'WLOSANY_X86_V3' then
		invoker.invoke(invoker.NOSTDIO, "modprobe", "option")
		plat4gOps['init'] = function () return true end
		plat4gOps['poweron'] = function (ison) return true end
		plat4gOps['netdev'] = function ()
			local ndev = "enp0s20f0u7"
			-- if netdev_exists(ndev) then return ndev end
			local okay, output = invoker.invoke(invoker.OUTPUT, "ip", "link", "show")
			if okay == 0 and type(output) == "string" then
				for line in string.gmatch(output, "[^\r\n]+") do
					local mdev = string.match(line, "^%d+: ([^:]+):")
					if mdev and #mdev >= 8 and mdev:sub(1, 3) == "enp" then
						ndev = mdev
						break
					end
				end
			end
			return ndev
		end
		return true
	end
	if bname == 'WLOSANY_BAU-B30-003' or bname == 'WLOSANY_WooLink2004' then
		plat4gOps['init'] = function ()
			-- TODO: add 4G modem poweron operation here
			return true
		end
		plat4gOps['poweron'] = function (ison)
			-- TODO: add 4G modem power on/off operation here
			return true
		end
		plat4gOps['netdev'] = function () return 'usb0' end
		return true
	end

	if bname == 'WLOSANY_RK3568_FORLINX' then
		plat4gOps['init'] = function ()
			local gpfx = "/sys/class/gpio/gpio101"
			local direct = gfmt("%s/direction", gpfx)
			local output = invoker.readfile(direct, invoker.TRIMEND)
			if output ~= "out" then
				invoker.invoke(0, "/bin/sh", "-c", "echo 101 > /sys/class/gpio/export")
				invoker.invoke(0, "/bin/sh", "-c", gfmt("echo out > %s", direct))
			end
			-- power on 4G module
			invoker.invoke(0, "/bin/sh", "-c", gfmt("echo 1 > %s/value", gpfx))
			return true
		end
		plat4gOps['poweron'] = function (ison)
			local cmd = gfmt("echo %s > /sys/class/gpio/gpio101value", ison and "1" or "0")
			invoker.invoke(0, "/bin/sh", "-c", cmd)
			return true
		end
		plat4gOps['netdev'] = function () return 'usb0' end
		return true
	end

	if bname == 'WLOSANY_RK3568' or bname == 'WLOSANY_RK3568_XDDL' then
		plat4gOps['init'] = function ()
			local gpfx = "/sys/class/gpio/gpio74"
			local direct = gfmt("%s/direction", gpfx)
			local output = invoker.readfile(direct, invoker.TRIMEND)
			if output ~= "out" then
				invoker.invoke(0, "/bin/sh", "-c", "echo 74 > /sys/class/gpio/export")
				invoker.invoke(0, "/bin/sh", "-c", gfmt("echo out > %s", direct))
			end
			-- power on 4G module
			invoker.invoke(0, "/bin/sh", "-c", gfmt("echo 1 > %s/value", gpfx))
			return true
		end
		plat4gOps['poweron'] = function (ison)
			local cmd = gfmt("echo %s > /sys/class/gpio/gpio74/value", ison and "1" or "0")
			invoker.invoke(0, "/bin/sh", "-c", cmd)
			return true
		end
		plat4gOps['netdev'] = function () return 'usb0' end
		return true
	end

	if bname == 'WLOSANY_VM1220T' then
		plat4gOps['init'] = function ()
			local gpfx = "/sys/class/gpio/gpio342"
			local direct = gfmt("%s/direction", gpfx)
			local output = invoker.readfile(direct, invoker.TRIMEND)
			if output ~= "out" then
				invoker.invoke(0, "/bin/sh", "-c", "echo 342 > /sys/class/gpio/export")
				invoker.invoke(0, "/bin/sh", "-c", gfmt("echo out > %s", direct))
			end
			-- power on 4G module
			invoker.invoke(0, "/bin/sh", "-c", gfmt("echo 0 > %s/value", gpfx))
			return true
		end
		plat4gOps['poweron'] = function (ison)
			local cmd = gfmt("echo %s > /sys/class/gpio/gpio342/value", ison and "0" or "1")
			invoker.invoke(0, "/bin/sh", "-c", cmd)
			return true
		end
		plat4gOps['netdev'] = function () return 'usb0' end
		return true
	end

	if bname == 'WLOSANY_RK3588' then
		plat4gOps['init'] = function ()
			local gpfx = "/sys/class/gpio/gpio120"
			local direct = gfmt("%s/direction", gpfx)
			local output = invoker.readfile(direct, invoker.TRIMEND)
			if output ~= "out" then
				invoker.invoke(0, "/bin/sh", "-c", "echo 120 > /sys/class/gpio/export")
				invoker.invoke(0, "/bin/sh", "-c", gfmt("echo out > %s", direct))
			end
			-- power on 4G module
			invoker.invoke(0, "/bin/sh", "-c", gfmt("echo 1 > %s/value", gpfx))
			return true
		end
		plat4gOps['poweron'] = function (ison)
			local cmd = gfmt("echo %s > /sys/class/gpio/gpio120/value", ison and "1" or "0")
			invoker.invoke(0, "/bin/sh", "-c", cmd)
			return true
		end
		plat4gOps['netdev'] = function () return 'usb0' end
		return true
	end

	if bname == 'WLOSANY_LC100' then
		plat4gOps['init'] = function ()
			local gpfx = "/sys/class/gpio/gpio132"
			local direct = gfmt("%s/direction", gpfx)
			local output = invoker.readfile(direct, invoker.TRIMEND)
			if output ~= "out" then
				invoker.invoke(0, "/bin/sh", "-c", "echo 132 > /sys/class/gpio/export")
				invoker.invoke(0, "/bin/sh", "-c", gfmt("echo out > %s", direct))
			end
			-- power on 4G module
			invoker.invoke(0, "/bin/sh", "-c", gfmt("echo 1 > %s/value", gpfx))
			return true
		end
		plat4gOps['poweron'] = function (ison)
			local cmd = gfmt("echo %s > /sys/class/gpio/gpio132/value", ison and "1" or "0")
			invoker.invoke(0, "/bin/sh", "-c", cmd)
			return true
		end
		plat4gOps['netdev'] = function () return 'usb0' end
		return true
	end

	if bname == 'WLOSANY_EM1000' then
		plat4gOps['init'] = function ()
			invoker.invoke(0, 'gpioset', '0', '25=0') -- power on 4G module
			return true
		end
		plat4gOps['poweron'] = function (ison)
			invoker.invoke(0, 'gpioset', '0', gfmt("25=%s", ison and "0" or "1"))
			return true
		end
		plat4gOps['netdev'] = function ()
			if netdev_exists('modem0') then return 'modem0' end
			return netdev_exists('eth0') and 'eth0' or 'usb0'
		end
		return true
	end

	io.stderr:write("Error, unsupported board_name for DEBIAN 4GNET.\n")
	io.stderr:flush()
	return false
end

local function preinit(pname, dns1, dns2)
	posix.unlink(modem4g.ICCID_FILE)
	if posix.access("/usr/lib/systemd/system/ModemManager.service") == 0 then
		invoker.invoke(invoker.NOSTDIO, "systemctl", "stop", "ModemManager")
		invoker.invoke(invoker.NOSTDIO, "systemctl", "disable", "ModemManager")
	end
	-- setup PATH environment variable
	posix.setenv('PATH', '/opt/lnxall_app/bin:/usr/bin:/usr/sbin:/bin:/sbin', true)
	posix.chdir("/")
	ulog.reinit(pname)
	invoker.setname(pname)

	if type(dns1) == "string" and invoker.isipv4(dns1) then g_dns[1] = dns1 end
	if type(dns2) == "string" and invoker.isipv4(dns2) then g_dns[2] = dns2 end

	-- wait until we have correct board_name
	while not check_platform() do invoker.msleep(5000) end

	check_resolv()
	local count = 0
	plat4gOps.init() -- initialize 4G module
	while network_disabled() do
		watch_resolv(30)
		if (invoker.uptime() - count) >= 30 then
			check_resolv()
			count = invoker.uptime()
		end
	end

	count = 0 -- reset to zero
	while not n4g_init(count) do
		watch_resolv(15)
		count = count + 1
	end
	return true
end

preinit('DEBIAN_4GNET', arg[1], arg[2])
ulog.log(ulog.LOG_INFO, "4G modem found, entering loop...")
mainloop() -- application hangs in the function
os.exit(2)
