#!/usr/bin/lua

-- Created by jiaqiang.ye@lnxall.com
-- miscellaneous operations for various 4G modem
-- 2024/08/21

local bit32 = require "bit32"
local cjson = require "cjson"
local posix = require "posix"
local invoker = require "invoker"

local mtab = {}
local usbid_list = {}
local omode = bit32.bor(posix.O_RDWR, posix.O_CLOEXEC, posix.O_NOCTTY, posix.O_NONBLOCK)
local mfmt = string.format

local g_4ginfo    = "/tmp/.modem_4ginfo.json"
mtab.ICCID_FILE   = '/tmp/simcard_iccid'
mtab.MODEM_INFO   = g_4ginfo

local theid = '2c7c:0904' -- EC800G
usbid_list[theid] = {}
usbid_list['2cb7:0a0b'] = {}
usbid_list['2cb7:0a0a'] = {}
usbid_list['2cb7:0a0c'] = {}

local function load_quectel_info()
	local qh = io.open(g_4ginfo)
	if not qh then return nil, nil end

	local qd = qh:read("*a")
	qh:close(); qh = nil
	if not qd then return nil, nil end
	local okay, jq = pcall(cjson.decode, qd)
	if not okay then return nil, nil end
	if type(jq) ~= "table" then return nil, nil end
	return jq.hassim0, jq.hassim1
end

local function set_baudrate(tdev)
	if posix.access(tdev) == 0 then
		invoker.invoke(invoker.NOSTDIO, 'stty', '-F', tdev, "raw", "115200", "cs8",
			"-hupcl", "-onlcr", "-iexten", "-echo", "-echoe", "-echok", "-echoctl", "-echoke")
		return true
	end
	return false
end

-- clear all remaining buffers
local function clear_tty_cache(fd)
	while true do
		local res = invoker.readfd(fd, 8192)
		if not res or string.len(res) == 0 then
			break
		end
	end
end

local function tty_write(ttydev, what, msec)
	if not ttydev then return nil end
	local tfd = posix.open(ttydev, omode)
	if type(tfd) ~= "number" or tfd < 0 then return nil end

	clear_tty_cache(tfd)
	local res = posix.write(tfd, what)
	if not msec then msec = 250 end
	invoker.msleep(msec)
	invoker.closefd(tfd); tfd = -1
	return res == #what
end

local function quectel_ec800g_checksim(ttydev)
	local r0, r1 = false, true
	local tfd = posix.open(ttydev, omode)
	if type(tfd) ~= "number" or tfd < 0 then return r0, r1 end

	clear_tty_cache(tfd)
	-- switch to internal SIM card
	posix.write(tfd, "AT+QDSIM=1\r\n")
	invoker.msleep(6000)

	posix.write(tfd, "AT+CPIN?\r\n")
	invoker.msleep(500)

	posix.write(tfd, "AT+ICCID\r\n")
	invoker.msleep(500)

	local res, res1, res0 = nil, {}, {}
	while true do
		res = invoker.readfd(tfd, 4096)
		if not res or string.len(res) == 0 then break end
		res1[#res1 + 1] = res
	end

	if #res1 > 0 then res1 = table.concat(res1) else res1 = "" end
	if string.find(res1, "CME ERROR: 58", 1, true) then res1 = "" end
	-- switch to external SIM card
	posix.write(tfd, "AT+QDSIM=0\r\n")
	invoker.msleep(6000)

	posix.write(tfd, "AT+CPIN?\r\n")
	invoker.msleep(500)

	posix.write(tfd, "AT+ICCID\r\n")
	invoker.msleep(500)

	while true do
		res = invoker.readfd(tfd, 4096)
		if not res or string.len(res) == 0 then break end
		res0[#res0 + 1] = res
	end
	invoker.closefd(tfd); tfd = -1

	if #res0 > 0 then res0 = table.concat(res0) else res0 = "" end
	if string.find(res0, "CME ERROR: 58", 1, true) then res0 = "" end

	r0 = string.find(res0, "CPIN: READY", 1, true) and true or false
	r1 = string.find(res1, "CPIN: READY", 1, true) and true or false
	id0 = string.match(res0, "ICCID:%s+([%d%a]+)")
	id1 = string.match(res1, "ICCID:%s+([%d%a]+)")

	--[[
	io.stdout:write(string.format("res0: %s===============================\n", res0))
	io.stdout:write(string.format("res1: %s===============================\n", res1))

	print("r0:   ", r0 and "true" or "false")
	print("ID0:  ", id0 and id0 or "nil")
	print("r1:   ", r1 and "true" or "false")
	print("ID1:  ", id1 and id1 or "nil")
	--]]
	return r0, r1, id0, id1
end

local function quectel_mc661_checksim(ttydev)
        local r0, r1 = false, true
        local tfd = posix.open(ttydev, omode)
        if type(tfd) ~= "number" or tfd < 0 then return r0, r1 end

		clear_tty_cache(tfd)
        -- switch to internal SIM card
        posix.write(tfd, "AT+GTDUALSIM=1\r\n")
        invoker.msleep(6000)

        posix.write(tfd, "AT+CPIN?\r\n")
        invoker.msleep(500)

        posix.write(tfd, "AT+CCID\r\n")
        invoker.msleep(500)

        local res, res1, res0 = nil, {}, {}
        while true do
                res = invoker.readfd(tfd, 4096)
				if not res or string.len(res) == 0 then break end
                res1[#res1 + 1] = res
        end

        if #res1 > 0 then res1 = table.concat(res1) else res1 = "" end
        -- switch to external SIM card
        posix.write(tfd, "AT+GTDUALSIM=0\r\n")
        invoker.msleep(6000)

        posix.write(tfd, "AT+CPIN?\r\n")
        invoker.msleep(500)

        posix.write(tfd, "AT+CCID\r\n")
        invoker.msleep(500)

        while true do
                res = invoker.readfd(tfd, 4096)
                if not res or string.len(res) == 0 then break end
                res0[#res0 + 1] = res
        end
        invoker.closefd(tfd); tfd = -1

        if #res0 > 0 then res0 = table.concat(res0) else res0 = "" end
        r0 = string.find(res0, "CPIN: READY", 1, true) and true or false
        r1 = string.find(res1, "CPIN: READY", 1, true) and true or false
        id0 = string.match(res0, "CCID:%s+([%d%a]+)")
        id1 = string.match(res1, "CCID:%s+([%d%a]+)")

        return r0, r1, id0, id1
end

-- 双卡双待/双卡单待, get working SIM no
local function quectel_getsimno(ttydev)
	if not ttydev then return nil end
	local tfd = posix.open(ttydev, omode)
	if type(tfd) ~= "number" or tfd < 0 then return nil end

	clear_tty_cache(tfd)
	posix.write(tfd, "AT+QDSIM?\r\n")
	invoker.msleep(1200) -- delay 1.2 second

	local res = nil
	while true do
		res = invoker.readfd(tfd, 8192)
		if not res or string.len(res) == 0 then break end
		local num = string.match(res, "QDSIM:[^\r\n%d]+(%d+)")
		--local num = string.match(res, "GTDUALSIM:[^\r\n%d]+(%d+)")
		if num then num = tostring(num) end
		if num then
			invoker.closefd(tfd); tfd = -1
			return num
		end
	end
	invoker.closefd(tfd); tfd = -1
	return nil
end

local function quectel_getsimno_mc661(ttydev)
        if not ttydev then return nil end
        local tfd = posix.open(ttydev, omode)
        if type(tfd) ~= "number" or tfd < 0 then return nil end

		clear_tty_cache(tfd)
        --posix.write(tfd, "AT+QDSIM?\r\n")
        posix.write(tfd, "AT+GTDUALSIM?\r\n")
        invoker.msleep(1200) -- delay 1.2 second

		local res = nil
        while true do
                res = invoker.readfd(tfd, 8192)
                if not res or string.len(res) == 0 then break end
                --local num = string.match(res, "QDSIM:[^\r\n%d]+(%d+)")
                local num = string.match(res, "GTDUALSIM:[^\r\n%d]+(%d+)")
                if num then num = tostring(num) end
                if num then
                        invoker.closefd(tfd); tfd = -1
                        return num
                end
        end
        invoker.closefd(tfd); tfd = -1
        return nil
end

-- 双卡双待/双卡单待, set working SIM no
local function quectel_setsimno(ttydev, id, disc)
	if not ttydev then return nil end
	local tfd = posix.open(ttydev, omode)
	if type(tfd) ~= "number" or tfd < 0 then return nil end

	if disc then
		posix.write(tfd, "AT+QIDEACT=1\r\n")
		invoker.msleep(600) -- delay 0.6 second
	end

	clear_tty_cache(tfd)
	posix.write(tfd, mfmt("AT+QDSIM=%d\r\n", id))
	invoker.msleep(2500) -- delay 2.5 second

	local res = invoker.readfd(tfd, 4096)
	invoker.closefd(tfd); tfd = -1
	if type(res) == "string" and string.find(res, "OK") then
		return true
	end
	return false
end

local function quectel_setsimno_mc661(ttydev, id, disc)
	if not ttydev then return nil end
    local tfd = posix.open(ttydev, omode)
	if type(tfd) ~= "number" or tfd < 0 then return nil end

	if disc then
		posix.write(tfd, "AT+QIDEACT=1\r\n")
		invoker.msleep(600) -- delay 0.6 second
 	end

	clear_tty_cache(tfd)
	posix.write(tfd, mfmt("AT+GTDUALSIM=%d\r\n", id))
	invoker.msleep(2500) -- delay 2.5 second

	local res = invoker.readfd(tfd, 4096)
	invoker.closefd(tfd); tfd = -1
	if type(res) == "string" and string.find(res, "OK") then
		return true
	end
	return false
end

usbid_list[theid]["init"] = function (self, ttydev)
	if self.nokaycnt == nil then
		self.cursim = 0
		self.nokaycnt = 0
	end

	local finit = false
	posix.unlink(mtab.ICCID_FILE) -- remove existing SIM ICCID file
	if posix.access(g_4ginfo) ~= 0 then finit = true end
	if self.hassim0 == nil then
		self.hassim0, self.hassim1 = load_quectel_info()
		if self.hassim0 == nil then finit = true end
	end

	set_baudrate(ttydev)
	if finit then
		tty_write(ttydev, "AT+QDSTYPE=0\r\n") -- work in 双卡单待 mode
		tty_write(ttydev, 'AT+QDSIMCFG="dsss",1\r\n')
		tty_write(ttydev, "AT+CFUN=1,1\r\n") -- take effect
		invoker.msleep(6000); set_baudrate(ttydev)
		local iccid0, iccid1 = nil, nil
		self.hassim0, self.hassim1, iccid0, iccid1 = quectel_ec800g_checksim(ttydev)

		local qinfo = cjson.encode({ ["hassim0"] = self.hassim0, ["hassim1"] = self.hassim1,
			["ICCID0"] = iccid0, ["ICCID1"] = iccid1 })
		local tagh = io.open(g_4ginfo, "wb")
		if tagh then
			tagh:write(qinfo)
			tagh:close(); tagh = nil
		end
	end

	-- check whether external SIM card exists
	if self.hassim0 then
		if finit then -- select external SIM card
			tty_write(ttydev, "AT+QDSIM=0\r\n")
			invoker.msleep(2500)
		end
		io.stdout:write("Found external SIM card.\n")
		io.stdout:flush()
	else
		io.stdout:write("External SIM card not found.\n")
		io.stdout:flush()
		tty_write(ttydev, "AT+QDSIM=1\r\n") -- select internal SIM card
		invoker.msleep(2500)
	end

	local simno = quectel_getsimno(ttydev)
	if simno then
		self.cursim = simno
		io.stdout:write(mfmt("Current SIMcard in use: %d\n", simno))
		io.stdout:flush()
		return true
	end
	return false
end

usbid_list[theid]["update"] = function (self, ttydev, netokay)
	if netokay then
		if self.nokaycnt > 0 then self.nokaycnt = 0 end
		return true
	end

	set_baudrate(ttydev)
	posix.unlink(mtab.ICCID_FILE)
	self.nokaycnt = self.nokaycnt + 1
	if self.nokaycnt >= 0x2 then
		local newno = self.cursim == 1 and 0 or 1
		quectel_setsimno(ttydev, newno, true)
		local simno = quectel_getsimno(ttydev) or -1
		self.nokaycnt = 0
		self.cursim = newno
		io.stdout:write(mfmt("Updated new SIM card: %d, %d\n", newno, simno))
		io.stdout:flush()
	else
		local simno = quectel_getsimno(ttydev) or -1
		if simno == self.cursim then
			quectel_setsimno(ttydev, self.cursim, true)
			io.stdout:write(mfmt("Current SIM card no: %d\n", self.cursim))
		else
			io.stdout:write(mfmt("Updated old SIM card: %d\n", self.cursim))
		end
		io.stdout:flush()
	end
	return false
end

usbid_list['2cb7:0a0c']['init'] = function (self, ttydev)
	-- write AT+GTUSBMODE=74 AT+CFUN=1,1
	tty_write(ttydev, "AT+GTUSBMODE=74\r\n")
	tty_write(ttydev, "AT+CFUN=1,1\r\n")
	invoker.msleep(6000)
end
usbid_list['2cb7:0a0a']['init'] = usbid_list['2cb7:0a0c']['init']

usbid_list['2cb7:0a0b']['init'] = function (self, ttydev)
	-- Read SIM ICCID 
	if self.nokaycnt == nil then
		self.cursim = 0
		self.nokaycnt = 0
	end

	posix.unlink(mtab.ICCID_FILE) -- remove existing SIM ICCID file
	set_baudrate(ttydev)
	local iccid0, iccid1 = nil, nil
	self.hassim0, self.hassim1, iccid0, iccid1 = quectel_mc661_checksim(ttydev)

	local qinfo = cjson.encode({ ["hassim0"] = self.hassim0, ["hassim1"] = self.hassim1,
		["ICCID0"] = iccid0, ["ICCID1"] = iccid1 })
	local tagh = io.open(g_4ginfo, "wb")
	if tagh then
		tagh:write(qinfo)
		tagh:close(); tagh = nil
	end
	
	-- check external sim CARD, switch to external SIM chard if exists
	if self.hassim0 then
		tty_write(ttydev, "AT+GTDUALSIM=0\r\n")
		invoker.msleep(2500)
		io.stdout:write("Found external SIM card.\n")
		io.stdout:flush()
	else
		io.stdout:write("External SIM card not found.\n")
		io.stdout:flush()
		tty_write(ttydev, "AT+GTDUALSIM=1\r\n") -- select internal SIM card
		invoker.msleep(2500)
	end

	local simno = quectel_getsimno_mc661(ttydev)
	if simno then
		self.cursim = simno
		io.stdout:write(mfmt("Current SIMcard in use: %d\n", simno))
		io.stdout:flush()
		return true
	end
	return false
end

usbid_list['2cb7:0a0c']['update'] = function (self, ttydev, netokay)
	os.exit(1)
end
usbid_list['2cb7:0a0a']['update'] = usbid_list['2cb7:0a0c']['update']

usbid_list['2cb7:0a0b']['update'] = function (self, ttydev, netokay)
	if netokay then
		if self.nokaycnt > 0 then self.nokaycnt = 0 end
        return true
	end

    set_baudrate(ttydev)
    posix.unlink(mtab.ICCID_FILE)
    self.nokaycnt = self.nokaycnt + 1
    if self.nokaycnt >= 0x2 then
		local newno = self.cursim == 1 and 0 or 1
		quectel_setsimno_mc661(ttydev, newno, true)
		local simno = quectel_getsimno(ttydev) or -1
		self.nokaycnt = 0
		self.cursim = newno
		io.stdout:write(mfmt("Updated new SIM card: %d, %d\n", newno, simno))
		io.stdout:flush()
	else
		local simno = quectel_getsimno(ttydev) or -1
		if simno == self.cursim then
			quectel_setsimno_mc661(ttydev, self.cursim, true)
			io.stdout:write(mfmt("Current SIM card no: %d\n", self.cursim))
		else
			io.stdout:write(mfmt("Updated old SIM card: %d\n", self.cursim))
		end
		io.stdout:flush()
	end
	return false
end

mtab.getmodem = function (usbid)
	if type(usbid) == "string" then
		return usbid_list[usbid]
	end
	return nil
end

mtab.hassimcard = function (ttydev)
	if posix.access(ttydev) ~= 0 then return nil end

	local tfd = posix.open(ttydev, omode)
	if type(tfd) ~= "number" or tfd < 0 then
		io.stderr:write(mfmt("Error, failed to open: %s\n", ttydev))
		io.stderr:flush()
		return false
	end

	clear_tty_cache(tfd)
	posix.write(tfd, "AT+CPIN?\r\n")
	invoker.msleep(1200) -- delay 1.2 second

	while true do
		local res = invoker.readfd(tfd, 8192)
		if not res or string.len(res) == 0 then break end

		-- io.stdout:write(mfmt("has CPIN: %s\n", res))
		-- io.stdout:flush()

		if string.find(res, "CPIN:%s+READY") then
			invoker.closefd(tfd); tfd = -1
			return true
		end
	end
	invoker.closefd(tfd); tfd = -1
	return false
end

return mtab
