--local toolName = "TNS|mLRS Configurator|TNE"
----------------------------------------------------------------------
-- Copyright (c) MLRS project
-- GPL3
-- https://www.gnu.org/licenses/gpl-3.0.de.html
-- OlliW @ www.olliw.eu
----------------------------------------------------------------------
-- Lua TOOLS script
----------------------------------------------------------------------
-- copy script to SCRIPTS\TOOLS folder on OpenTx SD card
-- works with mLRS v1.3.03 and later, mOTX v33

local version = '2025-01-12.01'

local required_tx_mLRS_version_int = 10303 -- 'v1.3.03'
local required_rx_mLRS_version_int = 10303 -- 'v1.3.03'


-- experimental
local paramLoadDeadTime_10ms = 300 -- 150 was a bit too short, also 200 was too short
local disableParamLoadErrorWarnings = false


----------------------------------------------------------------------
-- Screen
----------------------------------------------------------------------

local page_N1 = 9 -- number of options displayed in left column
local page_N = 18 -- number of options displayed on page

local function setupScreen()
    if LCD_H == 320 then
        page_N1 = 11
        page_N = 22
    else
        page_N1 = 9
        page_N = 18
    end
end    


----------------------------------------------------------------------
-- it would be so nice to have
----------------------------------------------------------------------

local function drawFilledTriangle(x0, y0, x1, y1, x2, y2, flags)
    if lcd.drawFilledTriangle == nil then return end
    lcd.drawFilledTriangle(x0, y0, x1, y1, x2, y2, flags)
end

local charSize = {}
charSize["a"] = 10
charSize["b"] = 10
charSize["c"] = 9
charSize["d"] = 10
charSize["e"] = 9
charSize["f"] = 7
charSize["g"] = 10
charSize["h"] = 10
charSize["i"] = 5
charSize["j"] = 6
charSize["k"] = 10
charSize["l"] = 5
charSize["m"] = 15
charSize["n"] = 10
charSize["o"] = 10
charSize["p"] = 10
charSize["q"] = 10
charSize["r"] = 7
charSize["s"] = 9
charSize["t"] = 6
charSize["u"] = 10
charSize["v"] = 9
charSize["w"] = 13
charSize["x"] = 9
charSize["y"] = 9
charSize["z"] = 9
charSize["0"] = 10
charSize["1"] = 10
charSize["2"] = 10
charSize["3"] = 10
charSize["4"] = 10
charSize["5"] = 10
charSize["6"] = 10
charSize["7"] = 10
charSize["8"] = 10
charSize["9"] = 10
charSize["_"] = 9
charSize["#"] = 11
charSize["-"] = 6
charSize["."] = 5

local function getCharWidth(c)
    if charSize[c] == nil then return 10 end
    return charSize[c]
end


----------------------------------------------------------------------
-- MBridge CRSF emulation
----------------------------------------------------------------------

local isConnected = nil
local cmdPush = nil
local cmdPop = nil

local MBRIDGE_COMMANDPACKET_STX  = 0xA0
local MBRIDGE_COMMANDPACKET_MASK = 0xE0

local MBRIDGE_CMD_TX_LINK_STATS_LEN  = 22
local MBRIDGE_CMD_DEVICE_ITEM_LEN    = 24
local MBRIDGE_CMD_PARAM_ITEM_LEN     = 24
local MBRIDGE_CMD_REQUEST_CMD_LEN    = 18
local MBRIDGE_CMD_INFO_LEN           = 24
local MBRIDGE_CMD_PARAM_SET_LEN      = 7
local MBRIDGE_CMD_MODELID_SET_LEN    = 3

local MBRIDGE_PARAM_TYPE_UINT8       = 0
local MBRIDGE_PARAM_TYPE_INT8        = 1
local MBRIDGE_PARAM_TYPE_UINT16      = 2
local MBRIDGE_PARAM_TYPE_INT16       = 3
local MBRIDGE_PARAM_TYPE_LIST        = 4
local MBRIDGE_PARAM_TYPE_STR6        = 5

local MBRIDGE_CMD_TX_LINK_STATS      = 2
local MBRIDGE_CMD_REQUEST_INFO       = 3
local MBRIDGE_CMD_DEVICE_ITEM_TX     = 4
local MBRIDGE_CMD_DEVICE_ITEM_RX     = 5
local MBRIDGE_CMD_PARAM_REQUEST_LIST = 6
local MBRIDGE_CMD_PARAM_ITEM         = 7
local MBRIDGE_CMD_PARAM_ITEM2        = 8
local MBRIDGE_CMD_PARAM_ITEM3        = 9
local MBRIDGE_CMD_REQUEST_CMD        = 10
local MBRIDGE_CMD_INFO               = 11
local MBRIDGE_CMD_PARAM_SET          = 12
local MBRIDGE_CMD_PARAM_STORE        = 13
local MBRIDGE_CMD_BIND_START         = 14
local MBRIDGE_CMD_BIND_STOP          = 15
local MBRIDGE_CMD_MODELID_SET        = 16
local MBRIDGE_CMD_SYSTEM_BOOTLOADER  = 17

local function mbridgeCmdLen(cmd)
    if cmd == MBRIDGE_CMD_TX_LINK_STATS then return MBRIDGE_CMD_TX_LINK_STATS_LEN; end
    if cmd == MBRIDGE_CMD_REQUEST_INFO then return 0; end
    if cmd == MBRIDGE_CMD_DEVICE_ITEM_TX then return MBRIDGE_CMD_DEVICE_ITEM_LEN; end
    if cmd == MBRIDGE_CMD_DEVICE_ITEM_RX then return MBRIDGE_CMD_DEVICE_ITEM_LEN; end
    if cmd == MBRIDGE_CMD_PARAM_REQUEST_LIST then return 0; end
    if cmd == MBRIDGE_CMD_PARAM_ITEM then return MBRIDGE_CMD_PARAM_ITEM_LEN; end
    if cmd == MBRIDGE_CMD_PARAM_ITEM2 then return MBRIDGE_CMD_PARAM_ITEM_LEN; end
    if cmd == MBRIDGE_CMD_PARAM_ITEM3 then return MBRIDGE_CMD_PARAM_ITEM_LEN; end
    if cmd == MBRIDGE_CMD_REQUEST_CMD then return MBRIDGE_CMD_REQUEST_CMD_LEN; end
    if cmd == MBRIDGE_CMD_INFO then return MBRIDGE_CMD_INFO_LEN; end
    if cmd == MBRIDGE_CMD_PARAM_SET then return MBRIDGE_CMD_PARAM_SET_LEN; end
    if cmd == MBRIDGE_CMD_PARAM_STORE then return 0; end
    if cmd == MBRIDGE_CMD_BIND_START then return 0; end
    if cmd == MBRIDGE_CMD_BIND_STOP then return 0; end
    if cmd == MBRIDGE_CMD_MODELID_SET then return MBRIDGE_CMD_MODELID_SET_LEN; end
    if cmd == MBRIDGE_CMD_SYSTEM_BOOTLOADER then return 0; end
    return 0;
end

local function crsfIsConnected()
    if getRSSI() ~= 0 then return true end
    return false
end

local function crsfCmdPush(cmd, payload)
    -- 'O', 'W', len/cmd, payload bytes
    local data = { 79, 87, cmd + MBRIDGE_COMMANDPACKET_STX }
    for i=1, mbridgeCmdLen(cmd) do data[#data + 1] = 0 end -- fill with zeros of correct length
    for i=1, #payload do data[3 + i] = payload[i] end -- fill in data
    -- crossfireTelemetryPush() extends it to
    -- 0xEE, len, 129, 'O', 'W', len/cmd, payload bytes, crc8
    return crossfireTelemetryPush(129, data)
end

local function crsfCmdPop()
    -- crossfireTelemetryPop() is invoked if
    -- address = RADIO_ADDRESS (0xEA) or UART_SYNC (0xC8)
    -- frame id != normal crsf telemetry sensor id
    -- 0xEE, len, 130, len/cmd, payload bytes, crc8
    local cmd, data = crossfireTelemetryPop()
    -- cmd = 130
    -- data = len/cmd, payload bytes
    if cmd == nil then return nil end
    if data == nil or data[1] == nil then return nil end -- Huston, we have a problem
    local command = data[1] - MBRIDGE_COMMANDPACKET_STX
    local res = {
        cmd = command,
        len = mbridgeCmdLen(command),
        payload = {}
    }
    for i=2, #data do res.payload[i-2] = data[i] end
    return res
end

local function mbridgeIsConnected()
    local LStats = mbridge.getLinkStats()
    if LStats.LQ > 0 then return true end
    return false
end

local function setupBridge()
    if mbridge == nil or not mbridge.enabled() then
        isConnected = crsfIsConnected
        cmdPush = crsfCmdPush
        cmdPop = crsfCmdPop -- can return nil
    else
        isConnected = mbridgeIsConnected
        cmdPush = mbridge.cmdPush
        cmdPop = mbridge.cmdPop -- can return nil
    end
end


----------------------------------------------------------------------
-- Info/Warning box
----------------------------------------------------------------------

local popup = false
local popup_text = ""
local popup_t_end_10ms = 0

local function setPopupWTmo(txt, tmo_10ms)
    if popup_t_end_10ms < 0 then return end -- blocked popup on display
    popup = true
    popup_text = txt
    popup_t_end_10ms = getTime() + tmo_10ms
end

local function setPopupBlocked(txt)
    popup = true
    popup_text = txt
    popup_t_end_10ms = -1
end

local function clearPopup()
    popup = false
    popup_t_end_10ms = 0
end

local function clearPopupIfBlocked()
    if popup and popup_t_end_10ms < 0 then clearPopup(); end
end

local function drawPopup()
    lcd.drawFilledRectangle(LCD_W/2-160-2, 74, 320+4, 84, TEXT_COLOR) --TITLE_BGCOLOR)
    lcd.drawFilledRectangle(LCD_W/2-160, 76, 320, 80, TITLE_BGCOLOR) --TEXT_BGCOLOR) --TITLE_BGCOLOR)

    local i = string.find(popup_text, "\n")
    local attr = MENU_TITLE_COLOR+MIDSIZE+CENTER
    if i == nil then
        lcd.drawText(LCD_W/2, 99, popup_text, attr)
    else
        local t1 = string.sub(popup_text, 1,i-1)
        local t2 = string.sub(popup_text, i+1)
        lcd.drawText(LCD_W/2, 85, t1, attr)
        lcd.drawText(LCD_W/2, 85+30, t2, attr)
    end
end

local function doPopup()
    if popup then
        drawPopup()
        if popup_t_end_10ms > 0 then
            local t_10ms = getTime()
            if t_10ms > popup_t_end_10ms then clearPopup() end
        end
    end
end


----------------------------------------------------------------------
-- helper to handle connect
----------------------------------------------------------------------

local connected = false
local connected_has_changed = false
local has_connected = false
local has_disconnected = false

local function doConnected()
    local is_connected = isConnected()

    connected_has_changed = false
    if is_connected ~= connected then connected_has_changed = true end

    has_connected = false
    if connected_has_changed and is_connected then has_connected = true end

    has_disconnected = false
    if connected_has_changed and not is_connected then has_disconnected = true end

    connected = is_connected
end


----------------------------------------------------------------------
-- variables for mBridge traffic
----------------------------------------------------------------------

local paramloop_t_last = 0
local DEVICE_ITEM_TX = nil
local DEVICE_ITEM_RX = nil
local DEVICE_INFO = nil
local DEVICE_PARAM_LIST = nil
local DEVICE_PARAM_LIST_expected_index = 0
local DEVICE_PARAM_LIST_current_index = -1
local DEVICE_PARAM_LIST_errors = 0
local DEVICE_PARAM_LIST_complete = false
local DEVICE_DOWNLOAD_is_running = true -- we start the script with this
local DEVICE_SAVE_t_last = 0


local function clearParams()
    DEVICE_ITEM_TX = nil
    DEVICE_ITEM_RX = nil
    DEVICE_INFO = nil
    DEVICE_PARAM_LIST = nil
    DEVICE_PARAM_LIST_expected_index = 0
    DEVICE_PARAM_LIST_current_index = -1
    DEVICE_PARAM_LIST_errors = 0
    DEVICE_PARAM_LIST_complete = false
    DEVICE_DOWNLOAD_is_running = true
end


local function paramsError(err)
    DEVICE_PARAM_LIST_errors = DEVICE_PARAM_LIST_errors + 1
end


----------------------------------------------------------------------
-- helper to convert command payloads
----------------------------------------------------------------------

local function mb_to_string(payload,pos,len)
    local str = ""
    for i = 0,len-1 do
        if payload[pos+i] == 0 then break end
        str = str .. string.char(payload[pos+i])
    end
    return str
end

local function mb_to_u8(payload, pos)
    return payload[pos]
end

local function mb_to_i8(payload, pos)
    local v = payload[pos+0]
    if v >= 128 then v = v - 256 end
    return v
end

local function mb_to_u16(payload, pos)
    return payload[pos+0] + payload[pos+1]*256
end

local function mb_to_i16(payload, pos)
    local v = payload[pos+0] + payload[pos+1]*256
    if v >= 32768 then v = v - 65536 end
    return v
end

local function mb_to_u24(payload, pos)
    return payload[pos+0] + payload[pos+1]*256 + payload[pos+2]*256*256
end

local function mb_to_u32(payload, pos)
    return payload[pos+0] + payload[pos+1]*256 + payload[pos+2]*256*256 + payload[pos+2]*256*256*256
end

local function mb_to_value(payload, pos, typ)
    if typ == MBRIDGE_PARAM_TYPE_UINT8 then -- UINT8
        return mb_to_u8(payload,pos)
    elseif typ == MBRIDGE_PARAM_TYPE_INT8 then -- INT8
        return mb_to_i8(payload,pos)
    elseif typ == MBRIDGE_PARAM_TYPE_UINT16 then -- UINT16
        return mb_to_u16(payload,pos)
    elseif typ == MBRIDGE_PARAM_TYPE_INT16 then -- INT16
        return mb_to_i16(payload,pos)
    elseif typ == MBRIDGE_PARAM_TYPE_LIST then -- LIST
        return payload[pos+0]
    end
    return 0
end

local function mb_to_value_or_str6(payload, pos, typ)
    if typ == 5 then --MBRIDGE_PARAM_TYPE_STR6 then
        return mb_to_string(payload,pos,6)
    else
        return mb_to_value(payload,pos,typ)
    end
end

local function mb_to_options(payload, pos, len)
    local str = ""
    for i = 0,len-1 do
        if payload[pos+i] == 0 then break end
        str = str .. string.char(payload[pos+i])
    end
    str = str .. ","
    local opt = {};
    for s in string.gmatch(str, "([^,]+)") do
        table.insert(opt, s)
    end
    return opt
end

local function mb_to_version_int(u16)
    local major = bit32.rshift(bit32.band(u16, 0xF000), 12)
    local minor = bit32.rshift(bit32.band(u16, 0x0FC0), 6)
    local patch = bit32.band(u16, 0x003F)
    return major * 10000 + minor * 100 + patch
end

local function mb_to_version_string(u16)
    local major = bit32.rshift(bit32.band(u16, 0xF000), 12)
    local minor = bit32.rshift(bit32.band(u16, 0x0FC0), 6)
    local patch = bit32.band(u16, 0x003F)
    return string.format("v%d.%d.%02d", major, minor, patch)
end

local function mb_to_u8_bits(payload, pos, bitpos, bitmask)
    local v = payload[pos]
    v = bit32.rshift(v, bitpos)
    v = bit32.band(v, bitmask)
    return v
end

local function mb_allowed_mask_editable(allowed_mask)
    -- if none or only one option allowed -> not editable
    if allowed_mask == 0 then return false; end
    if allowed_mask == 1 then return false; end
    if allowed_mask == 2 then return false; end
    if allowed_mask == 4 then return false; end
    if allowed_mask == 8 then return false; end
    if allowed_mask == 16 then return false; end
    if allowed_mask == 32 then return false; end
    if allowed_mask == 64 then return false; end
    if allowed_mask == 128 then return false; end
    if allowed_mask == 256 then return false; end
    return true
end

local diversity_list = {} -- used for displaying on main page bottom info section
diversity_list[0] = "enabled"
diversity_list[1] = "antenna1"
diversity_list[2] = "antenna2"
diversity_list[3] = "r:en. t:ant1"
diversity_list[4] = "r:en. t:ant2"

local freq_band_list = {} -- used for displaying on main page instead of received options
freq_band_list[0] = "2.4 GHz"
freq_band_list[1] = "915 MHz FCC"
freq_band_list[2] = "868 MHz"
freq_band_list[3] = "433 MHz"
freq_band_list[4] = "70 cm HAM"
freq_band_list[5] = "866 MHz IN"

local function getExceptNoFromChar(c)
    if (c >= 'a' and c <= 'z') then return (string.byte(c) - string.byte('a')) % 5; end
    if (c >= '0' and c <= '9') then return (string.byte(c) - string.byte('0')) % 5; end
    if (c == '_') then return 1; end
    if (c == '#') then return 2; end
    if (c == '-') then return 3; end
    if (c == '.') then return 4; end
    return 0
end

local function getExceptStrFromChar(c)
    local n = getExceptNoFromChar(c)
    if (n == 1) then return "/e1"; end
    if (n == 2) then return "/e6"; end
    if (n == 3) then return "/e11"; end
    if (n == 4) then return "/e13"; end
    return "/--"
end


----------------------------------------------------------------------
-- looper to send and read command frames
----------------------------------------------------------------------

local function doParamLoop()
    -- trigger getting device items and param items
    local t_10ms = getTime()
    if t_10ms - paramloop_t_last > 33 then -- was 10 = 100 ms
      paramloop_t_last = t_10ms
      if t_10ms < DEVICE_SAVE_t_last + paramLoadDeadTime_10ms then
          -- skip, we don't send a cmd if the last Save was recent
          -- TODO: we could make deadtime dependend on whether a receiver was connected before the Save
      elseif DEVICE_ITEM_TX == nil then
          cmdPush(MBRIDGE_CMD_REQUEST_INFO, {}) -- triggers sending DEVICE_ITEM_TX, DEVICE_ITEM_RX, INFO
          --cmdPush(MBRIDGE_CMD_REQUEST_CMD, {MBRIDGE_CMD_REQUEST_INFO)
          -- these should have been set when we nil-ed DEVICE_PARAM_LIST
          DEVICE_PARAM_LIST_expected_index = 0
          DEVICE_PARAM_LIST_current_index = -1
          DEVICE_PARAM_LIST_errors = 0
          DEVICE_PARAM_LIST_complete = false
      elseif DEVICE_PARAM_LIST == nil then
          if DEVICE_INFO ~= nil then -- wait for it to be populated
              DEVICE_PARAM_LIST = {}
              cmdPush(MBRIDGE_CMD_PARAM_REQUEST_LIST, {}) -- triggers sending full list of PARAM_ITEMs
              --cmdPush(MBRIDGE_CMD_REQUEST_CMD, {MBRIDGE_CMD_PARAM_REQUEST_LIST})
          end
      end
    end

    -- handle received commands
    for ijk = 1,24 do -- handle only up to 24 per lua cycle
        local cmd = cmdPop()
        if cmd == nil then break end
        if cmd.cmd == MBRIDGE_CMD_DEVICE_ITEM_TX then
            -- MBRIDGE_CMD_DEVICE_ITEM_TX
            DEVICE_ITEM_TX = cmd
            DEVICE_ITEM_TX.version_u16 = mb_to_u16(cmd.payload, 0)
            DEVICE_ITEM_TX.setuplayout_u16 = mb_to_u16(cmd.payload, 2)
            DEVICE_ITEM_TX.name = mb_to_string(cmd.payload, 4, 20)
            DEVICE_ITEM_TX.version_int = mb_to_version_int(DEVICE_ITEM_TX.version_u16)
            DEVICE_ITEM_TX.version_str = mb_to_version_string(DEVICE_ITEM_TX.version_u16)
            DEVICE_ITEM_TX.setuplayout_int = mb_to_version_int(DEVICE_ITEM_TX.setuplayout_u16)
        elseif cmd.cmd == MBRIDGE_CMD_DEVICE_ITEM_RX then
            -- MBRIDGE_CMD_DEVICE_ITEM_RX
            DEVICE_ITEM_RX = cmd
            DEVICE_ITEM_RX.version_u16 = mb_to_u16(cmd.payload, 0)
            DEVICE_ITEM_RX.setuplayout_u16 = mb_to_u16(cmd.payload, 2)
            DEVICE_ITEM_RX.name = mb_to_string(cmd.payload, 4, 20)
            DEVICE_ITEM_RX.version_int = mb_to_version_int(DEVICE_ITEM_RX.version_u16)
            DEVICE_ITEM_RX.version_str = mb_to_version_string(DEVICE_ITEM_RX.version_u16)
            DEVICE_ITEM_RX.setuplayout_int = mb_to_version_int(DEVICE_ITEM_RX.setuplayout_u16)
        elseif cmd.cmd == MBRIDGE_CMD_INFO then
            -- MBRIDGE_CMD_INFO
            DEVICE_INFO = cmd
            DEVICE_INFO.receiver_sensitivity = mb_to_i16(cmd.payload, 0)
            DEVICE_INFO.has_status = mb_to_u8_bits(cmd.payload, 2, 0, 0x01)
            DEVICE_INFO.binding = mb_to_u8_bits(cmd.payload, 2, 1, 0x01)
            DEVICE_INFO.LQ_low = 0 -- mb_to_u8_bits(cmd.payload, 2, 3, 0x03)
            DEVICE_INFO.tx_power_dbm = mb_to_i8(cmd.payload, 3)
            DEVICE_INFO.rx_power_dbm = mb_to_i8(cmd.payload, 4)
            DEVICE_INFO.rx_available = mb_to_u8_bits(cmd.payload, 5, 0, 0x1)
            --DEVICE_INFO.tx_diversity = mb_to_u8_bits(cmd.payload, 5, 1, 0x3)
            --DEVICE_INFO.rx_diversity = mb_to_u8_bits(cmd.payload, 5, 3, 0x3)
            DEVICE_INFO.tx_config_id = mb_to_u8(cmd.payload, 6)
            DEVICE_INFO.tx_diversity = mb_to_u8_bits(cmd.payload, 7, 0, 0x0F)
            DEVICE_INFO.rx_diversity = mb_to_u8_bits(cmd.payload, 7, 4, 0x0F)
        elseif cmd.cmd == MBRIDGE_CMD_PARAM_ITEM then
            -- MBRIDGE_CMD_PARAM_ITEM
            local index = cmd.payload[0]
            if index ~= DEVICE_PARAM_LIST_expected_index and index ~= 255 then
                paramsError()
            end
            DEVICE_PARAM_LIST_current_index = index -- inform potential Item2/3 calls
            DEVICE_PARAM_LIST_expected_index = index + 1 -- prepare for next
            if DEVICE_PARAM_LIST == nil then
                paramsError()
            elseif index < 128 then
                DEVICE_PARAM_LIST[index] = cmd
                DEVICE_PARAM_LIST[index].typ = mb_to_u8(cmd.payload, 1)
                DEVICE_PARAM_LIST[index].name = mb_to_string(cmd.payload, 2, 16)
                DEVICE_PARAM_LIST[index].value = mb_to_value_or_str6(cmd.payload, 18, DEVICE_PARAM_LIST[index].typ)
                DEVICE_PARAM_LIST[index].min = 0
                DEVICE_PARAM_LIST[index].max = 0
                DEVICE_PARAM_LIST[index].unit = ""
                DEVICE_PARAM_LIST[index].options = {}
                DEVICE_PARAM_LIST[index].allowed_mask = 65536
                DEVICE_PARAM_LIST[index].editable = true
            elseif index == 255 then -- EOL (end of list :)
                if DEVICE_PARAM_LIST_errors == 0 then
                    DEVICE_PARAM_LIST_complete = true
                elseif disableParamLoadErrorWarnings then -- ignore any errors
                    DEVICE_PARAM_LIST_complete = true
                else
                    -- Huston, we have a proble,
                    DEVICE_PARAM_LIST_complete = false
                    setPopupWTmo("Param Upload Errors ("..tostring(DEVICE_PARAM_LIST_errors)..")!\nTry Reload", 200)
                end
                DEVICE_DOWNLOAD_is_running = false
            else
                paramsError()
            end
        elseif cmd.cmd == MBRIDGE_CMD_PARAM_ITEM2 then
            -- MBRIDGE_CMD_PARAM_ITEM2
            local index = cmd.payload[0]
            if index ~= DEVICE_PARAM_LIST_current_index then
                paramsError()
            elseif DEVICE_PARAM_LIST == nil or DEVICE_PARAM_LIST[index] == nil then
                paramsError()
            else
                if DEVICE_PARAM_LIST[index].typ < MBRIDGE_PARAM_TYPE_LIST then
                    DEVICE_PARAM_LIST[index].min = mb_to_value(cmd.payload, 1, DEVICE_PARAM_LIST[index].typ)
                    DEVICE_PARAM_LIST[index].max = mb_to_value(cmd.payload, 3, DEVICE_PARAM_LIST[index].typ)
                    DEVICE_PARAM_LIST[index].unit = mb_to_string(cmd.payload, 7, 6)
                elseif DEVICE_PARAM_LIST[index].typ == MBRIDGE_PARAM_TYPE_LIST then
                    DEVICE_PARAM_LIST[index].allowed_mask = mb_to_u16(cmd.payload, 1)
                    DEVICE_PARAM_LIST[index].options = mb_to_options(cmd.payload, 3, 21)
                    DEVICE_PARAM_LIST[index].item2payload = cmd.payload
                    DEVICE_PARAM_LIST[index].min = 0
                    DEVICE_PARAM_LIST[index].max = #DEVICE_PARAM_LIST[index].options - 1
                    DEVICE_PARAM_LIST[index].editable = mb_allowed_mask_editable(DEVICE_PARAM_LIST[index].allowed_mask)
                elseif DEVICE_PARAM_LIST[index].typ == MBRIDGE_PARAM_TYPE_STR6 then
                    -- nothing to do, is send but hasn't any content
                else
                    paramsError()
                end
            end
        elseif cmd.cmd == MBRIDGE_CMD_PARAM_ITEM3 then
            -- MBRIDGE_CMD_PARAM_ITEM3
            local index = cmd.payload[0]
            local is_item4 = false
            if (index >= 128) then -- this is actually ITEM4
                index = index - 128;
                is_item4 = true
            end
            if index ~= DEVICE_PARAM_LIST_current_index then
                paramsError()
            elseif DEVICE_PARAM_LIST == nil or DEVICE_PARAM_LIST[index] == nil then
                paramsError()
            elseif DEVICE_PARAM_LIST[index].typ ~= MBRIDGE_PARAM_TYPE_LIST then
                paramsError()
            elseif DEVICE_PARAM_LIST[index].item2payload == nil then
                paramsError()
            elseif is_item4 and DEVICE_PARAM_LIST[index].item3payload == nil then
                paramsError()
            else
                local s = DEVICE_PARAM_LIST[index].item2payload
                if not is_item4 then
                    DEVICE_PARAM_LIST[index].item3payload = cmd.payload
                    for i=1,23 do s[23+i] = cmd.payload[i] end
                    DEVICE_PARAM_LIST[index].options = mb_to_options(s, 3, 21+23)
                else
                    local s3 = DEVICE_PARAM_LIST[index].item3payload
                    for i=1,23 do s[23+i] = s3[i]; s[23+23+i] = cmd.payload[i]; end
                    DEVICE_PARAM_LIST[index].options = mb_to_options(s, 3, 21+23+23)
                end
                DEVICE_PARAM_LIST[index].max = #DEVICE_PARAM_LIST[index].options - 1
                s = nil
            end
        end
        cmd = nil
    end --for
end


local function sendParamSet(idx)
    if not DEVICE_PARAM_LIST_complete then return end -- needed here??
    local p = DEVICE_PARAM_LIST[idx]
    if p.typ < MBRIDGE_PARAM_TYPE_LIST then
        cmdPush(MBRIDGE_CMD_PARAM_SET, {idx, p.value})
    elseif p.typ == MBRIDGE_PARAM_TYPE_LIST then
        cmdPush(MBRIDGE_CMD_PARAM_SET, {idx, p.value})
    elseif p.typ == MBRIDGE_PARAM_TYPE_STR6 then
        local cmd = {idx}
        for i = 1,6 do
            cmd[i+1] = string.byte(string.sub(p.value, i,i))
        end
        cmdPush(MBRIDGE_CMD_PARAM_SET, cmd)
    end
end


local function sendParamStore()
    if not DEVICE_PARAM_LIST_complete then return end -- needed here??
    cmdPush(MBRIDGE_CMD_PARAM_STORE, {})
    DEVICE_SAVE_t_last = getTime()
    setPopupWTmo("Save Parameters", 250)
end


local function sendBind()
    --if not DEVICE_PARAM_LIST_complete then return end -- needed here??
    if DEVICE_DOWNLOAD_is_running then return end
    cmdPush(MBRIDGE_CMD_BIND_START, {})
    setPopupBlocked("Binding")
end


local function sendBoot()
    --if not DEVICE_PARAM_LIST_complete then return end -- needed here??
    if DEVICE_DOWNLOAD_is_running then return end
    cmdPush(MBRIDGE_CMD_SYSTEM_BOOTLOADER, {})
    setPopupBlocked("In System Bootloader")
end


local function checkBind()
    if DEVICE_DOWNLOAD_is_running then return end
    if DEVICE_INFO ~= nil and DEVICE_INFO.has_status == 1 and DEVICE_INFO.binding == 1 then
        setPopupBlocked("Binding")
    end
end


local function checkLQ()
--[[ 
-- the mechanism doesn't work well when a receiver is connected while the Lua is running
-- since in the first second the LQ may not be high
-- so we disable it for until a better approach is found
    if DEVICE_DOWNLOAD_is_running or not connected then 
        if popup and popup_t_end_10ms < 0 and popup_text == "LQ is low" then clearPopup() end    
        return 
    end
    if DEVICE_INFO ~= nil and DEVICE_INFO.has_status == 1 and DEVICE_INFO.LQ_low > 1 then
        setPopupBlocked("LQ is low")
    end
--]]    
end


----------------------------------------------------------------------
-- Edit stuff
----------------------------------------------------------------------

local PAGE_MAIN = 0
local PAGE_EDIT_TX = 1
local PAGE_EDIT_RX = 2
local PAGE_TOOLS = 3
local page_nr = PAGE_MAIN -- 0: main, 1: edit Tx, 2: edit Rx, 3: tools

local BindPhrase_idx = 0 -- idxes of options on main page
local Mode_idx = 1
local RFBand_idx = 2
local RFOrtho_idx = 3
local EditTx_idx = 4
local EditRx_idx = 5
local Save_idx = 6
local Reload_idx = 7
local Bind_idx = 8
local Tools_idx = 9
local PAGE_MAIN_CURSOR_IDX_MAX = 9

-- note: for common parameters it is assumed that cursor idx = param idx
local COMMON_PARAM_IDX_MAX = 3

local cursor_idx = EditTx_idx
local edit = false
local option_value = 0

local cursor_pidx = 0 -- parameter idx which corresponds to the current cursor_idx
local page_param_cnt = 0 -- number of parameters available on page

local cursor_x_idx = 0 -- index into string for string edits
local bindphrase_chars = "abcdefghijklmnopqrstuvwxyz0123456789_#-."


local function cur_attr(idx) -- used in menu
    local attr = TEXT_COLOR
    if cursor_idx == idx then
        attr = attr + INVERS
        if edit then attr = attr + BLINK end
    end
    return attr
end


local function cur_attr_x(idx, x_idx) -- for Bind Phrase character editing
    local attr = TEXT_COLOR
    if DEVICE_PARAM_LIST_complete and cursor_idx == idx then
        if edit then
            if cursor_x_idx == x_idx then attr = attr + BLINK + INVERS end
        else
            attr = attr + INVERS
        end
    end
    return attr
end


local function cur_attr_p(idx, pidx) -- used for parameters
    local attr = cur_attr(idx)
    if DEVICE_PARAM_LIST_complete and not DEVICE_PARAM_LIST[pidx].editable then
        lcd.setColor(CUSTOM_COLOR, GREY)
        attr = CUSTOM_COLOR
    end
    return attr
end


local function param_value_inc(idx)
    if not DEVICE_PARAM_LIST_complete then return end -- needed here??
    local p = DEVICE_PARAM_LIST[idx]
    if p.typ < MBRIDGE_PARAM_TYPE_LIST then
        p.value = p.value + 1
    elseif p.typ == MBRIDGE_PARAM_TYPE_LIST then
        local value = p.value
        while value <= p.max do
            value = value + 1
            local m = bit32.lshift(1,value)
            if bit32.btest(m, p.allowed_mask) then p.value = value; break end
        end
    end
    if p.value > p.max then p.value = p.max end
    DEVICE_PARAM_LIST[idx].value = p.value
end


local function param_value_dec(idx)
    if not DEVICE_PARAM_LIST_complete then return end -- needed here??
    local p = DEVICE_PARAM_LIST[idx]
    if p.typ < MBRIDGE_PARAM_TYPE_LIST then
        p.value = p.value - 1
    elseif p.typ == MBRIDGE_PARAM_TYPE_LIST then
        local value = p.value
        while value >= p.min do
            value = value - 1
            local m = bit32.lshift(1,value)
            if bit32.btest(m, p.allowed_mask) then p.value = value; break end
        end
    end
    if p.value < p.min then p.value = p.min end
    DEVICE_PARAM_LIST[idx].value = p.value
end


local function param_str6_inc(idx)
    if not DEVICE_PARAM_LIST_complete then return end -- needed here??
    local p = DEVICE_PARAM_LIST[idx]
    if p.typ == MBRIDGE_PARAM_TYPE_STR6 then
        local c = string.sub(p.value, cursor_x_idx+1, cursor_x_idx+1)
        local i = string.find(bindphrase_chars, c, 1, true) -- true for plain search
        i = i + 1
        if i > string.len(bindphrase_chars) then i = 1 end
        c = string.sub(bindphrase_chars, i,i)
        p.value = string.sub(p.value, 1, cursor_x_idx) .. c .. string.sub(p.value, cursor_x_idx+2, string.len(p.value))
    end
    DEVICE_PARAM_LIST[idx].value = p.value
end


local function param_str6_dec(idx)
    if not DEVICE_PARAM_LIST_complete then return end -- needed here??
    local p = DEVICE_PARAM_LIST[idx]
    if p.typ == MBRIDGE_PARAM_TYPE_STR6 then
        local c = string.sub(p.value, cursor_x_idx+1, cursor_x_idx+1)
        local i = string.find(bindphrase_chars, c, 1, true) -- true for plain search
        i = i - 1
        if i < 1 then i = string.len(bindphrase_chars) end
        c = string.sub(bindphrase_chars, i,i)
        p.value = string.sub(p.value, 1, cursor_x_idx) .. c .. string.sub(p.value, cursor_x_idx+2, string.len(p.value))
    end
    DEVICE_PARAM_LIST[idx].value = p.value
end


local function param_str6_next(idx)
    if not DEVICE_PARAM_LIST_complete then return false end -- needed here??
    local p = DEVICE_PARAM_LIST[idx]
    if p.typ == MBRIDGE_PARAM_TYPE_STR6 then
        cursor_x_idx = cursor_x_idx + 1
        if cursor_x_idx >= string.len(p.value) then
            return true -- last char
        end
    end
    return false
end


local function param_focusable(idx)
    if not DEVICE_PARAM_LIST_complete then return false end -- needed here??
    local p = DEVICE_PARAM_LIST[idx]
    if p == nil then return false end
    if p.editable == nil then return false end
    return p.editable
end


----------------------------------------------------------------------
-- Page Edit Tx/Rx
----------------------------------------------------------------------

local top_idx = 0 -- index of first displayed option


local function drawPageEdit(page_str)
    if LCD_H == 320 then
        page_N1 = 11 -- number of options displayed in left column
        page_N = 22 -- number of options displayed on page
    else
        page_N1 = 9
        page_N = 18
    end
    
    local x, y;

    y = 35
    if page_str == "Tx" then
        if DEVICE_INFO ~= nil then
            lcd.drawText(5, y, "Tx - "..tostring(DEVICE_INFO.tx_config_id)..":", TEXT_COLOR)
        else
            lcd.drawText(5, y, "Tx - ?:", TEXT_COLOR)
        end
    else
        lcd.drawText(5, y, page_str..":", TEXT_COLOR)
    end

    y = 60
    local dy = 21
    local y0 = y

    if cursor_idx < top_idx then top_idx = cursor_idx end
    if cursor_idx >= top_idx + page_N then top_idx = cursor_idx - page_N + 1 end

    local idx = 0
    page_param_cnt = 0
    for pidx = 2, #DEVICE_PARAM_LIST do
        local p = DEVICE_PARAM_LIST[pidx]
        if p ~= nil and string.sub(p.name,1,2) == page_str and p.allowed_mask > 0 then
        local name = string.sub(p.name, 4)

        if idx >= top_idx and idx < top_idx + page_N then
            local shifted_idx = idx - top_idx

            y = y0 + shifted_idx * dy

            local xofs = 0
            if shifted_idx >= page_N1 then y = y - page_N1*dy; xofs = 230 end

            lcd.drawText(10+xofs, y, name, TEXT_COLOR)
            if p.typ < MBRIDGE_PARAM_TYPE_LIST then
                lcd.drawText(140+xofs, y, p.value.." "..p.unit, cur_attr_p(idx, pidx))
            elseif p.typ == MBRIDGE_PARAM_TYPE_LIST then
                lcd.drawText(140+xofs, y, p.options[p.value+1], cur_attr_p(idx, pidx))
            end
        end

        if cursor_idx == idx then cursor_pidx = pidx end

        idx = idx + 1
      end
    end

    page_param_cnt = idx

    local x_mid = LCD_W/2 - 5
    if top_idx > 0 then
        local y_base = y0 - 4
        --lcd.drawFilledTriangle(x_mid-6, y_base, x_mid+6, y_base, x_mid, y_base-6, TEXT_COLOR)
        drawFilledTriangle(x_mid-6, y_base, x_mid+6, y_base, x_mid, y_base-6, TEXT_COLOR)
    end
    if page_param_cnt > top_idx + page_N then
        local y_base = y0 + page_N1*dy + 4
        --lcd.drawFilledTriangle(x_mid-6, y_base, x_mid+6, y_base, x_mid, y_base+6, TEXT_COLOR)
        drawFilledTriangle(x_mid-6, y_base, x_mid+6, y_base, x_mid, y_base+6, TEXT_COLOR)
    end
end


local function doPageEdit(event, page_str)
    lcd.drawFilledRectangle(0, 0, LCD_W, 30, TITLE_BGCOLOR)
    lcd.drawText(5, 5, "mLRS Configurator: Edit "..page_str, MENU_TITLE_COLOR)

    if event == EVT_VIRTUAL_EXIT and not edit then
        page_nr = PAGE_MAIN
        cursor_idx = EditTx_idx
        return
    end

    drawPageEdit(page_str) -- call before event handling, ensures that page_param_cnt, cursor_pidx are set

    if not edit then
        if event == EVT_VIRTUAL_EXIT then
        elseif event == EVT_VIRTUAL_ENTER then
            edit = true
        elseif event == EVT_VIRTUAL_NEXT then
            cursor_idx = cursor_idx + 1
            if cursor_idx >= page_param_cnt then cursor_idx = page_param_cnt - 1 end
        elseif event == EVT_VIRTUAL_PREV then
            cursor_idx = cursor_idx - 1
            if cursor_idx < 0 then cursor_idx = 0 end
        end
    else
        if event == EVT_VIRTUAL_EXIT then
            sendParamSet(cursor_pidx)
            edit = false
        elseif event == EVT_VIRTUAL_ENTER then
            sendParamSet(cursor_pidx)
            edit = false
        elseif event == EVT_VIRTUAL_NEXT then
            param_value_inc(cursor_pidx)
        elseif event == EVT_VIRTUAL_PREV then
            param_value_dec(cursor_pidx)
        end
    end

end


----------------------------------------------------------------------
-- Page Tools
----------------------------------------------------------------------

local Tools_Boot_idx = 0 -- idxes of options on tools page
local PAGE_TOOLS_CURSOR_IDX_MAX = 0

local function drawPageTools()
    local x, y;

    y = 60
    lcd.drawText(10, y, "System Bootloader", cur_attr(Tools_Boot_idx))
end


local function doPageTools(event)
    lcd.drawFilledRectangle(0, 0, LCD_W, 30, TITLE_BGCOLOR)
    lcd.drawText(5, 5, "mLRS Configurator: Tools Page", MENU_TITLE_COLOR)

    drawPageTools()

    if event == EVT_VIRTUAL_ENTER then
        if cursor_idx == Tools_Boot_idx then -- Boot
            page_nr = PAGE_MAIN
            cursor_idx = EditTx_idx
            sendBoot()
        end
    elseif event == EVT_VIRTUAL_EXIT then
        page_nr = PAGE_MAIN
        cursor_idx = EditTx_idx
        return
    elseif event == EVT_VIRTUAL_NEXT then
        cursor_idx = cursor_idx + 1
        if cursor_idx > PAGE_TOOLS_CURSOR_IDX_MAX then cursor_idx = PAGE_TOOLS_CURSOR_IDX_MAX end
    elseif event == EVT_VIRTUAL_PREV then
        cursor_idx = cursor_idx - 1
        if cursor_idx < 0 then cursor_idx = 0 end
    end
end


----------------------------------------------------------------------
-- Page Main
----------------------------------------------------------------------

local function drawSetuplayoutWarning(txt)
    local y = 210
    lcd.drawFilledRectangle(LCD_W/2-210-2, y, 420+4, 50+4, TEXT_COLOR) --TITLE_BGCOLOR)
    lcd.drawFilledRectangle(LCD_W/2-210, y+2, 420, 50, TITLE_BGCOLOR) --TEXT_BGCOLOR) --TITLE_BGCOLOR)
    local attr = MENU_TITLE_COLOR+CENTER
    local s = txt
    local i = string.find(s, "\n")
    local lines = 0
    while i ~= nil do
        local s1 = string.sub(s, 1,i-1)
        lcd.drawText(LCD_W/2, y+6 + lines, s1, attr)
        s = string.sub(s, i+1)
        i = string.find(s, "\n")
        lines = lines + 21
    end
    lcd.drawText(LCD_W/2, y+6 + lines, s, attr)
end


local function drawPageMain()
    local x, y;

    lcd.setColor(CUSTOM_COLOR, RED)

    y = 35
    lcd.drawText(5, y, "Tx:", TEXT_COLOR)
    if DEVICE_ITEM_TX == nil then
        lcd.drawText(35, y, "---", TEXT_COLOR)
    else
        lcd.drawText(35, y, DEVICE_ITEM_TX.name, TEXT_COLOR+SMLSIZE)
        lcd.drawText(35, y+16, DEVICE_ITEM_TX.version_str, TEXT_COLOR+SMLSIZE)
        if DEVICE_INFO ~= nil then
            lcd.drawText(35, y+32, "ConfigId "..tostring(DEVICE_INFO.tx_config_id), TEXT_COLOR+SMLSIZE)
        end
    end

    lcd.drawText(240, y, "Rx:", TEXT_COLOR)
    if not DEVICE_PARAM_LIST_complete then
        -- don't do anything
    elseif not connected then
        lcd.drawText(270, y, "not connected", TEXT_COLOR)
    elseif DEVICE_ITEM_RX == nil then
        lcd.drawText(270, y, "---", TEXT_COLOR)
    else
        lcd.drawText(270, y, DEVICE_ITEM_RX.name, TEXT_COLOR+SMLSIZE)
        lcd.drawText(270, y+16, DEVICE_ITEM_RX.version_str, TEXT_COLOR+SMLSIZE)
    end

    local version_error = false
    if DEVICE_ITEM_TX ~= nil and DEVICE_ITEM_TX.version_int < required_tx_mLRS_version_int then
        version_error = true
        popup_text = "Tx version not supported\nby this Lua script!"
    end
    if DEVICE_ITEM_RX ~= nil and connected and DEVICE_ITEM_RX.version_int < required_rx_mLRS_version_int then
        version_error = true
        popup_text = "Rx version not supported\nby this Lua script!"
    end
    if version_error then
        drawPopup()
        return
    end

    y = 95 --90
    lcd.drawText(10, y, "Bind Phrase", TEXT_COLOR)
    if DEVICE_PARAM_LIST_complete then
        local x = 140
        for i = 1,6 do
            local c = string.sub(DEVICE_PARAM_LIST[0].value, i, i) -- param_idx = 0 = BindPhrase
            local attr = cur_attr_x(0, i-1)
            lcd.drawText(x, y, c, attr)
            --x = x + lcd.getTextWidth(c,1,attr)+1
            x = x + getCharWidth(c) + 1
            if i == 6 and DEVICE_PARAM_LIST[2].value == 0 then -- do only for 2.4GHz band
                lcd.drawText(140 + 70, y, getExceptStrFromChar(c), TEXT_COLOR)
            end
        end
    end

    lcd.drawText(10, y + 21, "Mode", TEXT_COLOR)
    if DEVICE_PARAM_LIST_complete then
        local p = DEVICE_PARAM_LIST[1] -- param_idx = 1 = Mode
        if p.options[p.value+1] ~= nil then
            lcd.drawText(140, y+21, p.options[p.value+1], cur_attr_p(Mode_idx,1))
        end
    end

    lcd.drawText(10, y + 2*21, "RF Band", TEXT_COLOR)
    if DEVICE_PARAM_LIST_complete then
        local p = DEVICE_PARAM_LIST[2] -- param_idx = 2 = RfBand
        if p.options[p.value+1] ~= nil then
            --lcd.drawText(240+80, y, p.options[p.value+1], cur_attr(2))
            if p.value <= #freq_band_list then
                lcd.drawText(140, y + 2*21, freq_band_list[p.value], cur_attr_p(RFBand_idx,2))
            else
                lcd.drawText(140, y + 2*21, p.options[p.value+1], cur_attr_p(RFBand_idx,2))
            end
        end
    end

    if DEVICE_PARAM_LIST_complete and DEVICE_PARAM_LIST[3].allowed_mask > 0 then
        lcd.drawText(270, y, "Ortho", TEXT_COLOR)
        local p = DEVICE_PARAM_LIST[3] -- param_idx = 3 = RfOrtho
        if p.options[p.value+1] ~= nil then
            lcd.drawText(330, y, p.options[p.value+1], cur_attr_p(RFOrtho_idx,3))
        end
    end

    y = 171 --166
    lcd.drawText(10, y, "Edit Tx", cur_attr(EditTx_idx))
    if not connected then
        lcd.drawText(10 + 80, y, "Edit Rx", TEXT_DISABLE_COLOR)
    else
        lcd.drawText(10 + 80, y, "Edit Rx", cur_attr(EditRx_idx))
    end
    lcd.drawText(10 + 160, y, "Save", cur_attr(Save_idx))
    lcd.drawText(10 + 225, y, "Reload", cur_attr(Reload_idx))
    lcd.drawText(10 + 305, y, "Bind", cur_attr(Bind_idx))
    lcd.drawText(10 + 365, y, "Tools", cur_attr(Tools_idx))

    -- show overview of some selected parameters
    y = 210 --05
    lcd.setColor(CUSTOM_COLOR, GREY)
    lcd.drawFilledRectangle(0, y-5, LCD_W, 1, CUSTOM_COLOR)

    --if not DEVICE_PARAM_LIST_complete then
    if DEVICE_DOWNLOAD_is_running then
        lcd.drawText(130, y+20, "parameters loading ...", TEXT_COLOR+BLINK+INVERS)
        return
    end

    lcd.drawText(10, y, "Tx Power", TEXT_COLOR)
    lcd.drawText(10, y+20, "Tx Diversity", TEXT_COLOR)
    if DEVICE_INFO ~= nil then
        lcd.drawText(140, y, tostring(DEVICE_INFO.tx_power_dbm).." dBm", TEXT_COLOR)
        if DEVICE_INFO.tx_diversity <= #diversity_list then
            lcd.drawText(140, y+20, diversity_list[DEVICE_INFO.tx_diversity], TEXT_COLOR)
        else
            lcd.drawText(140, y+20, "?", TEXT_COLOR)
        end
    else
        lcd.drawText(140, y, "---", TEXT_COLOR)
        lcd.drawText(140, y+20, "---", TEXT_COLOR)
    end

    local rx_attr = TEXT_COLOR
    if not connected then
        rx_attr = TEXT_DISABLE_COLOR
    end
    lcd.drawText(10+240, y, "Rx Power", rx_attr)
    lcd.drawText(10+240, y+20, "Rx Diversity", rx_attr)
    if DEVICE_INFO ~= nil and connected then
        lcd.drawText(140+240, y, tostring(DEVICE_INFO.rx_power_dbm).." dBm", rx_attr)
        if DEVICE_INFO.rx_diversity <= #diversity_list then
            lcd.drawText(140+240, y+20, diversity_list[DEVICE_INFO.rx_diversity], rx_attr)
        else
            lcd.drawText(140+240, y+20, "?", rx_attr)
        end
    else
        lcd.drawText(140+240, y, "---", rx_attr)
        lcd.drawText(140+240, y+20, "---", rx_attr)
    end

    y = y + 2*20
    lcd.drawText(10, y, "Sensitivity", TEXT_COLOR)
    if DEVICE_INFO ~= nil then
        lcd.drawText(140, y, tostring(DEVICE_INFO.receiver_sensitivity).." dBm", TEXT_COLOR)
    else
        lcd.drawText(140, y, "---", TEXT_COLOR)
    end

    -- setup layout warning
    if DEVICE_ITEM_TX ~= nil and DEVICE_ITEM_RX ~= nil and connected and DEVICE_PARAM_LIST_complete and
           (DEVICE_ITEM_TX.setuplayout_int > 515 or DEVICE_ITEM_RX.setuplayout_int > 515) then -- 515 is old 335
        if DEVICE_ITEM_TX.setuplayout_int < DEVICE_ITEM_RX.setuplayout_int then
            drawSetuplayoutWarning("Tx param version smaller than Rx param version.\nPlease update Tx firmware!")
        elseif DEVICE_ITEM_RX.setuplayout_int < DEVICE_ITEM_TX.setuplayout_int then
            drawSetuplayoutWarning("Rx param version smaller than Tx param version.\nPlease update Rx firmware!")
        end
    end
end


local function doPageMain(event)
    lcd.drawFilledRectangle(0, 0, LCD_W, 30, TITLE_BGCOLOR)
    lcd.drawText(5, 5, "mLRS Configurator: Main Page", MENU_TITLE_COLOR)
    lcd.drawText(LCD_W-1, 0, version, MENU_TITLE_COLOR+TINSIZE+RIGHT)

    if not edit then
        if event == EVT_VIRTUAL_EXIT then
            -- nothing to do
        elseif event == EVT_VIRTUAL_ENTER then
            if cursor_idx == EditTx_idx and DEVICE_PARAM_LIST_complete then -- EditTX pressed
                page_nr = PAGE_EDIT_TX
                cursor_idx = 0
                top_idx = 0
                return
            elseif cursor_idx == EditRx_idx and DEVICE_PARAM_LIST_complete then -- EditRX pressed
                page_nr = PAGE_EDIT_RX
                cursor_idx = 0
                top_idx = 0
                return
            elseif cursor_idx == Save_idx and DEVICE_PARAM_LIST_complete then -- Save pressed
                sendParamStore()
                clearParams()
            elseif cursor_idx == Reload_idx then -- Reload pressed
                clearParams()
            elseif cursor_idx == Bind_idx then -- Bind pressed
                sendBind()
            elseif cursor_idx == Tools_idx and DEVICE_PARAM_LIST_complete then -- Tools pressed
                page_nr = PAGE_TOOLS
                cursor_idx = 0
                top_idx = 0
                return
            elseif DEVICE_PARAM_LIST_complete then -- edit option
                cursor_x_idx = 0
                edit = true
            end
        elseif event == EVT_VIRTUAL_NEXT then -- and DEVICE_PARAM_LIST_complete then
            cursor_idx = cursor_idx + 1
            if cursor_idx > PAGE_MAIN_CURSOR_IDX_MAX then cursor_idx = PAGE_MAIN_CURSOR_IDX_MAX end

            if cursor_idx == Mode_idx and not param_focusable(1) then cursor_idx = cursor_idx + 1 end
            if cursor_idx == RFBand_idx and not param_focusable(2) then cursor_idx = cursor_idx + 1 end
            if cursor_idx == RFOrtho_idx and not param_focusable(3) then cursor_idx = cursor_idx + 1 end
            if cursor_idx == EditRx_idx and not connected then cursor_idx = cursor_idx + 1 end
        elseif event == EVT_VIRTUAL_PREV then -- and DEVICE_PARAM_LIST_complete then
            cursor_idx = cursor_idx - 1
            if cursor_idx < 0 then cursor_idx = 0 end

            if cursor_idx == EditRx_idx and not connected then cursor_idx = cursor_idx - 1 end
            if cursor_idx == RFOrtho_idx and not param_focusable(3) then cursor_idx = cursor_idx - 1 end
            if cursor_idx == RFBand_idx and not param_focusable(2) then cursor_idx = cursor_idx - 1 end
            if cursor_idx == Mode_idx and not param_focusable(1) then cursor_idx = cursor_idx - 1 end
        end
    else
        if event == EVT_VIRTUAL_EXIT then
            if cursor_idx <= COMMON_PARAM_IDX_MAX then -- BindPhrase, Mode, RF Band, RF Ortho
                sendParamSet(cursor_idx)
            end
            edit = false
        elseif event == EVT_VIRTUAL_ENTER then
            if cursor_idx == BindPhrase_idx then -- BindPhrase
                if param_str6_next(0) then
                    sendParamSet(0)
                    edit = false
                end
            elseif cursor_idx <= COMMON_PARAM_IDX_MAX then -- Mode, RF Band, RF Ortho
                sendParamSet(cursor_idx)
                edit = false
            else
                edit = false
            end
        elseif event == EVT_VIRTUAL_NEXT then
            if cursor_idx == BindPhrase_idx then -- BindPhrase
                param_str6_inc(0)
            elseif cursor_idx <= COMMON_PARAM_IDX_MAX then -- Mode, RF Band, RF Ortho
                param_value_inc(cursor_idx)
            end
        elseif event == EVT_VIRTUAL_PREV then
            if cursor_idx == BindPhrase_idx then -- BindPhrase
                param_str6_dec(0)
            elseif cursor_idx <= COMMON_PARAM_IDX_MAX then -- Mode, RF Band, RF Ortho
                param_value_dec(cursor_idx)
            end
        end
    end

    drawPageMain()
end


----------------------------------------------------------------------
----------------------------------------------------------------------

local isEdgeTx = false

local function Do(event)
    doConnected()

    if has_connected then
        clearParams()
        if not popup then setPopupWTmo("Receiver connected!", 100) end
        clearPopupIfBlocked()
    end
    if has_disconnected then
        if not popup then setPopupWTmo("Receiver\nhas disconnected!", 100) end
    end
    if not connected and page_nr == PAGE_EDIT_RX then
        page_nr = PAGE_MAIN
        cursor_idx = EditTx_idx
    end

    doParamLoop()

    -- OpenTx: must display
    -- EdgeTx: don't display in param upload, EdgeTx is super slow
    if isEdgeTx and DEVICE_DOWNLOAD_is_running then return end

    lcd.clear()

    if page_nr == PAGE_EDIT_TX then
        doPageEdit(event,"Tx")
    elseif page_nr == PAGE_EDIT_RX then
        doPageEdit(event,"Rx")
    elseif page_nr == PAGE_TOOLS then
        doPageTools(event)
    else
        doPageMain(event)
    end

    checkBind()
    checkLQ()
    doPopup()
end


----------------------------------------------------------------------
-- Script OTX Interface
----------------------------------------------------------------------

local function scriptInit()
    local ver, radio, maj, minor, rev, osname = getVersion()
    isEdgeTx = (osname == 'EdgeTX')

    setupScreen()
    setupBridge()

    DEVICE_DOWNLOAD_is_running = true -- we start the script with this
    local tnow_10ms = getTime()
    if tnow_10ms < 300 then
        DEVICE_SAVE_t_last = 300 - tnow_10ms -- treat script start like a Save
    else
        DEVICE_SAVE_t_last = 0
    end
end


local function scriptRun(event)
    if event == nil then
        error("Cannot be run as a model script!")
        return 2
    end
    if mbridge == nil or not mbridge.enabled() then
        if model.getModule(0).Type ~= 5 and model.getModule(1).Type ~= 5 then
            error("mLRS not accessible: mBridge or CRSF not enabled!")
            return 2
        end
    end

    if isConnected == nil or cmdPush == nil or cmdPop == nil then --just to be sure for sure
        error("Unclear issue with mBridge or CRSF!")
        return 2
    end

    if not edit and page_nr == PAGE_MAIN then
        if event == EVT_VIRTUAL_EXIT then
            return 2
        end
    end

    Do(event)

    return 0
end

return { init=scriptInit, run=scriptRun }
