--[[
Lua script to send a recieve very basic MAVLink telemetry over a
Rockblock 9704 SBD satellite modem
Requires https://github.com/stephendade/rockblock2mav at the GCS end

Setup:
This script requires 1 serial port, 1 scripting serial port, 1 relay output and 1 GPIO input

ftp put /home/stephen/Documents/UAVCode/ardupilot/libraries/AP_Scripting/applets/RockBlock-9704.lua APM/Scripts/RockBlock-9704.lua

Usage:
Use the MAVLink High Latency Control ("link hl on|off" in MAVProxy) to control
whether to send/receive or not (or use "force_hl_enable")

Written by Stephen Dade (stephen_dade@hotmail.com)


]]--

-- number of millsec that GCS telemetry has been lost for
local link_lost_for = 0

-- Params
local PARAM_TABLE_KEY = 108
local PARAM_TABLE_PREFIX = "RK9_"

-- bind a parameter to a variable
function bind_param(name)
    local p = Parameter()
    assert(p:init(name), string.format('could not find %s parameter', name))
    return p
 end

 -- add a parameter and bind it to a variable
 function bind_add_param(name, idx, default_value)
    assert(param:add_param(PARAM_TABLE_KEY, idx, name, default_value), string.format('could not add param %s', name))
    return bind_param(PARAM_TABLE_PREFIX .. name)
 end

 -- setup RK9 specific parameters
assert(param:add_table(PARAM_TABLE_KEY, PARAM_TABLE_PREFIX, 9), 'could not add param table')
--[[
  // @Param: RK9_FORCEHL
  // @DisplayName: Force enable High Latency mode
  // @Description: Automatically enables High Latency mode if not already enabled
  // @Values: 0:Disabled,1:Enabled,2:Enabled on telemetry loss
  // @User: Standard
--]]
RK9_FORCEHL     = bind_add_param('FORCEHL', 1, 0)

--[[
  // @Param: RK9_PERIOD
  // @DisplayName: Update rate
  // @Description: When in High Latency mode, send Rockblock updates every N seconds
  // @Range: 0 600
  // @Units: s
  // @User: Standard
--]]
RK9_PERIOD     = bind_add_param('PERIOD', 2, 30)

--[[
  // @Param: RK9_DEBUG
  // @DisplayName: Display Rockblock debugging text
  // @Description: Sends Rockblock debug text to GCS via statustexts
  // @Values: 0:Disabled,1:Enabled
  // @User: Standard
--]]
RK9_DEBUG     = bind_add_param('DEBUG', 3, 0)

--[[
  // @Param: RK9_ENABLE
  // @DisplayName: Enable Message transmission
  // @Description: Enables the Rockblock sending and recieving
  // @Values: 0:Disabled,1:Enabled
  // @User: Standard
--]]
RK9_ENABLE     = bind_add_param('ENABLE', 4, 1)

--[[
  // @Param: RK9_TIMEOUT
  // @DisplayName: GCS timeout to start sendin Rockblock messages
  // @Description: If RK9_FORCEHL=2, this is the number of seconds of GCS timeout until High Latency mode is auto-enabled
  // @Range: 0 600
  // @Units: s
  // @User: Standard
--]]
RK9_TIMEOUT     = bind_add_param('TIMEOUT', 5, 5)

--[[
  // @Param: RK9_SERPORT
  // @DisplayName: Rockblock Serial Port
  // @Description: Serial port number to which the Rockblock is connected.This is the index of the SERIALn_ ports that are set to 28 for "scripting"
  // @Range: 0 8
  // @User: Standard
--]]
RK9_SERPORT     = bind_add_param('SERPORT', 6, 0)

--[[
  // @Param: RK9_SCRPORT
  // @DisplayName: Rockblock Scripting Serial Port
  // @Description: Scripting Serial port number to which the Rockblock is connected for HL2 messages This is the index of the SCR_SDEV ports that are set to 2 for "MavlinkHL"
  // @Range: 0 8
  // @User: Standard
--]]
RK9_SCRPORT     = bind_add_param('SCRPORT', 7, 0)

--[[
  // @Param: RK9_RELAY
  // @DisplayName: Rockblock Power Relay
  // @Description: RELAYn output to control Rockblock power. This connects to I_EN on the Rockblock header.
  // @Range: 1 8
  // @User: Standard
--]]
RK9_RELAY     = bind_add_param('RELAY', 8, 1)

--[[
  // @Param: RK9_BOOTED
  // @DisplayName: Rockblock booted feedback
  // @Description: SERVOn GPIO channel that reads the Rockblock booted state. This connects to I_BTD on the Rockblock header. Requires SERVON_FUNCTION=-1
  // @Range: 50 110
  // @User: Standard
--]]
RK9_BOOTED     = bind_add_param('BOOTED', 9, 52)

local pinboot = RK9_BOOTED:get()
if pinboot then
    gpio:pinMode(math.floor(pinboot), 0) -- set AUX 3 (servo11) to input I_BTD SERVO11_FUNCTION=-1
end

-- Modem States
local ModemState = {
    POWER0 = 0, --not powered
    POWER1 = 1,
    BOOTED1 = 3,
    BOOTED2 = 4,
    BOOTED3 = 5,
    API_CONFIGURED = 6,
    SIM_CONFIGURED = 7,
    OPERATIONAL_CONFIGURED = 8,
    TOPIC_RECIEVED = 9
}

-- The delay between loops
local loop_delay_ms = 200

local base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
-- Create lookup table for bit patterns to base64 chars
local base64_bit_to_char = {}
for i = 0, 63 do
    local c = base64:sub(i + 1, i + 1)
    local pattern = ''
    local val = i
    for j = 5, 0, -1 do
        pattern = pattern .. (val >= 2^j and '1' or '0')
        if val >= 2^j then val = val - 2^j end
    end
    base64_bit_to_char[pattern] = c
end

-- Create inverse lookup table for base64 chars to bit patterns
local base64_char_to_bits = {}
for pattern, char in pairs(base64_bit_to_char) do
    base64_char_to_bits[char] = pattern
end

local function base64_encode(input)
    return ((input:gsub('.', function(x)
        local r, b_char = '', x:byte()
        for i = 8, 1, -1 do r = r .. (b_char % 2 ^ i - b_char % 2 ^ (i - 1) > 0 and '1' or '0') end
        return r
    end) .. '0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
        if (#x < 6) then return '' end
        return base64_bit_to_char[x:sub(1,6)]
    end) .. ({ '', '==', '=' })[#input % 3 + 1])
end

local function base64_decode(input)
    -- Strip invalid characters and padding
    input = input:gsub('[^'..base64..'=]', '')
    input = input:gsub('=', '')

    -- Convert base64 chars to bit sequence
    local bits = input:gsub('.', function(x)
        return base64_char_to_bits[x] or ''
    end)

    -- Convert 8-bit sequences to binary string
    local result = ""
    for i = 1, #bits, 8 do
        local byte = bits:sub(i, i+7)
        -- Pad with zeros if needed
        byte = byte .. string.rep('0', 8 - #byte)
        local value = 0
        for j = 1, 8 do
            value = value * 2 + tonumber(byte:sub(j,j))
        end
        result = result .. string.char(value)
    end
    return result
end

--[[
   xmodem CRC implementation thanks to https://github.com/cloudwu/skynet
   under MIT license
--]]
XMODEMCRC16Lookup = {
    [0] = 0x0000,
           0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
    0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
    0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6,
    0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de,
    0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485,
    0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d,
    0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4,
    0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc,
    0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823,
    0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b,
    0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12,
    0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a,
    0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41,
    0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49,
    0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70,
    0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78,
    0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f,
    0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067,
    0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e,
    0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256,
    0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d,
    0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405,
    0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c,
    0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634,
    0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab,
    0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3,
    0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a,
    0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92,
    0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9,
    0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1,
    0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8,
    0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0,
}

function crc_xmodem_lua_table(bytes)
    local t = XMODEMCRC16Lookup
    local b = string.byte
    local crc = 0
    for i=1,#bytes do
        crc = ((crc<<8) & 0xffff) ~ t[((crc>>8)~b(bytes, i))]
    end
    return crc
end

--[[
Lua Object for managing the RockBlock modem
--]]
local function RockblockModem9704()
    -- public fields
    local self = {
        state = ModemState.POWER0,
        raw_topic_id = nil,
        message_id = 0,
        request_reference = 0,
        cur_message = nil,
        rxbuffer = "",
        txbuffer = nil,
        txbufferlen = 0,
        bars = 0,
        port = nil,
        scr_port = nil,
        last_status_check = 0,
        status_interval = 10000, -- 10 seconds
        last_mavlink_send = 0,
        scr_string = ""
    }

    -- Encode message with CRC
    function self.encode_message(message)
        local crc = crc_xmodem_lua_table(message)
        local crc_bytes = string.char((crc >> 8) & 0xFF) .. string.char(crc & 0xFF)
        local result = message .. crc_bytes
        local encoded = base64_encode(result)
        return encoded
    end

    -- Decode message and verify CRC
    function self.decode_message(encoded_message)
        local decoded = base64_decode(encoded_message)
        local msg_len = #decoded
        local crc_received = string.sub(decoded, msg_len-1)
        local message_data = string.sub(decoded, 1, msg_len-2)
        local crc_calculated = crc_xmodem_lua_table(message_data)
        local crc_calculated_bytes = string.char((crc_calculated >> 8) & 0xFF) ..
                                     string.char(crc_calculated & 0xFF)
        -- print out the message data and fields
        if RK9_DEBUG:get() == 1 then
            gcs:send_text(6, string.format("Rockblock: Received CRC: %02X %02X", crc_received:byte(1), crc_received:byte(2)))
            gcs:send_text(6, string.format("Rockblock: Calculated CRC: %02X %02X", crc_calculated_bytes:byte(1), crc_calculated_bytes:byte(2)))
        end
        return message_data, crc_received, crc_calculated_bytes
    end


    -- Send GET command to the modem
    function self.send_get_command(command)
        self.cur_message = command
        local command_str = string.format("GET %s {}\r", command)
        if RK9_DEBUG:get() == 1 then
            gcs:send_text(6, "Rockblock: TX: " .. command_str:gsub("\r", ""))
        end
        if self.port then
            self.port:writestring(command_str)
        end
    end

    -- Send PUT command to the modem
    function self.send_put_command(command, options)
        local command_str = string.format("PUT %s {%s}\r", command, options)
        if RK9_DEBUG:get() == 1 then
            gcs:send_text(6, "Rockblock: TX: " .. command_str:gsub("\r", ""))
        end
        self.cur_message = command
        if self.port then
            self.port:writestring(command_str)
        end
    end

    -- Process provisioning message
    function self.process_provisioning_message(message, target_name)
        -- Find the entire object containing the target_name
        local object_str = message:match('{[^}]-"topic_name"%s*:%s*"' .. target_name .. '"[^}]-}')
        if not object_str then
            return nil
        end

        -- Extract topic_id from that object
        local topic_id = object_str:match('"topic_id"%s*:%s*(%d+)')
        return topic_id and tonumber(topic_id) or nil
    end

    -- Process JSON response from the modem
    -- This is a simplified JSON parser for ArduPilot Lua
    function self.process_json(json_str, target)
        local result = {}

        -- Extract key fields using pattern matching
        result.active_version_major = tonumber(string.match(json_str, "\"major\":([%d]+)") or "0")
        result.constellation_visible = string.match(json_str, "\"constellation_visible\":([%a]+)") == "true"
        result.signal_bars = tonumber(string.match(json_str, "\"signal_bars\":([%d]+)") or "0")
        result.message_response = string.match(json_str, "\"message_response\":\"([%w_]+)\"")
        result.message_id = tonumber(string.match(json_str, "\"message_id\":([%d]+)") or "0")
        result.final_mo_status = string.match(json_str, "\"final_mo_status\":\"([%w_]+)\"")
        result.data = string.match(json_str, "\"data\":\"([%w+/=]+)\"")

        -- Find RAW topic id
        if target == "messageProvisioning" then
            local topic_id = self.process_provisioning_message(json_str, "RAW")
            if topic_id then
                result.raw_topic_id = topic_id
            else
                gcs:send_text(2, "Rockblock: Failed to find RAW topic ID. Check modem is activated.")
            end
        end

        return result
    end

    -- Process a line received from the modem
    function self.process_line(line)
        -- Extract response code and target
        local response_code = tonumber(string.match(line, "^(%d+)") or "0")
        local target = string.match(line, "%d+ ([%w_]+)")
        local json_start = line:find("{")

        if not json_start then return end

        local json_str = line:sub(json_start)
        local json_response = self.process_json(json_str, target)

        -- Command acknowledgment
        if RK9_DEBUG:get() == 1 then
            gcs:send_text(6, "Rockblock: Command " .. target .. " acknowledged")
        end
        if target == self.cur_message then
            self.cur_message = nil
        end

        -- Handle state transitions
        if response_code == 400 then
            self.state = ModemState.BOOTED2
            return
        end

        if target == "apiVersion" and (response_code == 200 or response_code == 299 or response_code == 402) and
           json_response.active_version_major >= 1 then
            self.state = ModemState.BOOTED3
        elseif target == "apiVersion" then
            self.state = ModemState.BOOTED2
        elseif target == "hwInfo" then
            self.state = ModemState.API_CONFIGURED
            gcs:send_text(5, "Rockblock: Modem detected")
        elseif target == "simConfig" then
            self.state = ModemState.SIM_CONFIGURED
        elseif target == "operationalState" then
            self.state = ModemState.OPERATIONAL_CONFIGURED
            gcs:send_text(5, "Rockblock: Modem configured")
        elseif target == "constellationState" then
            self.bars = json_response.signal_bars or 0
            if json_response.constellation_visible and RK9_DEBUG:get() == 1 then
                gcs:send_text(5, "Rockblock: Signal bars: " .. self.bars .. "/5")
            end
            self.last_status_check = millis()
        elseif target == "messageProvisioning" then
            if json_response.raw_topic_id then
                self.raw_topic_id = json_response.raw_topic_id
                if RK9_DEBUG:get() == 1 then
                    gcs:send_text(5, "Rockblock: RAW topic ID: " .. self.raw_topic_id)
                end
                self.state = ModemState.TOPIC_RECIEVED
            else
                gcs:send_text(2, "Rockblock: Failed to find RAW topic ID. Check modem is activated.")
            end
        elseif target == "messageOriginate" then
            if json_response.message_response == "message_accepted" then
                self.message_id = json_response.message_id

                -- Send the segment
                if self.txbuffer then
                    self.send_put_command("messageOriginateSegment",
                                    string.format("\"topic_id\":%d, \"message_id\":%d, \"segment_length\":%d, \"segment_start\":0, \"data\":\"%s\"",
                                                 self.raw_topic_id, self.message_id, self.txbufferlen, self.txbuffer))
                end
            end
        elseif target == "messageOriginateStatus" then
            if json_response.final_mo_status == "mo_ack_received" and json_response.message_id == self.message_id then
                gcs:send_text(5, "Rockblock: Message " .. self.message_id .. " sent successfully")
            elseif json_response.message_id == self.message_id then
                gcs:send_text(2, "Rockblock: Message " .. self.message_id .. " failed to send" ..
                               " with status: " .. json_response.final_mo_status)
            end
            self.txbuffer = nil
        elseif target == "messageTerminateSegment" and response_code == 299 then
            if json_response.data then
                if RK9_DEBUG:get() == 1 then
                    gcs:send_text(6, "Rockblock: Data received: " .. json_response.data)
                end
                local message_data, crc_received, crc_calculated = self.decode_message(json_response.data)

                -- Check CRC
                local crc_ok = true
                for i = 1, #crc_received do
                    if crc_received:byte(i) ~= crc_calculated:byte(i) then
                        crc_ok = false
                        break
                    end
                end

                if not crc_ok then
                    gcs:send_text(2, "Rockblock: CRC mismatch in received message")
                else
                    self.scr_port:writestring(message_data)
                end
            end
        end
    end

    -- Send a message through the modem
    function self.send_message(message)
        if RK9_ENABLE:get() == 0 then
            if RK9_DEBUG:get() == 1 then
                gcs:send_text(6, "Rockblock: Rockblock sending disabled by RK9_ENABLE param")
            end
            return false
        end

        if not self.raw_topic_id then
            if RK9_DEBUG:get() == 1 then
                gcs:send_text(6, "Rockblock: No RAW topic ID available")
            end
            return false
        end

        if self.bars == 0 then
            if RK9_DEBUG:get() == 1 then
                gcs:send_text(6, "Rockblock: No signal bars available")
            end
            return false
        end

        if self.txbuffer then
            if RK9_DEBUG:get() == 1 then
                gcs:send_text(6, "Rockblock: Modem already processing a message")
            end
            return false
        end

        -- Send messageOriginate
        self.request_reference = self.request_reference + 1
        self.txbuffer = self.encode_message(message)
        -- note the buffer len is before the base64 encoding takes place
        self.txbufferlen = #message + 2  -- +2 for CRC
        self.send_put_command("messageOriginate",
                        string.format("\"topic_id\":%d, \"message_length\":%d, \"request_reference\":%d",
                                     self.raw_topic_id, self.txbufferlen, self.request_reference))

        return true
    end

    -- Read from the serial port and process data
    function self.modem_process()
        if not self.port then return end

        -- Read available data from rockblock
        local n_bytes = math.min(self.port:available():toint(), 70)
        local rxstring = self.port:readstring(n_bytes)

        -- Read available data from scripting serial port
        n_bytes = self.scr_port:available():toint()
        self.scr_string = self.scr_string .. self.scr_port:readstring(n_bytes)
        -- if there's more than 150 bytes in the buffer, just trim to the last 150 bytes
        if #self.scr_string > 150 then
            self.scr_string = self.scr_string:sub(-150)
        end

        -- Send pending messages if we're in HL mode and it's been RK9_PERIOD:get() since last send
        if #self.scr_string > 52 then
            local now = (millis():tofloat() * 0.001)
            if now - self.last_mavlink_send > RK9_PERIOD:get() and gcs:get_high_latency_status() then
                if self.state == ModemState.TOPIC_RECIEVED and self.bars > 0 and not self.txbuffer then
                    --iterate through the string to find 0xFD (MAVLink start of packet)
                    while #self.scr_string > 10 do
                        local fd_pos = self.scr_string:find(string.char(0xFD))
                        --ensure it's a HL2 packet by looking at the message ID (0xEB)
                        if fd_pos and #self.scr_string > (fd_pos + 12) and self.scr_string:byte(fd_pos + 7) == 0xEB then
                            local scr_string_aligned = self.scr_string:sub(fd_pos, fd_pos + 51)
                            if RK9_DEBUG:get() == 1 then
                                gcs:send_text(6, "Rockblock: Sending message of len " .. #scr_string_aligned)
                            end
                            self.send_message(scr_string_aligned)
                            self.scr_string = ""
                        elseif fd_pos then
                            -- remove up to and including the FD
                            self.scr_string = self.scr_string:sub(fd_pos + 1)
                        else
                            -- no FD found, clear the buffer
                            self.scr_string = ""
                        end
                    end
                elseif self.state ~= ModemState.TOPIC_RECIEVED then
                    gcs:send_text(2, "Rockblock: Cannot send message, modem not configured")
                elseif self.bars == 0 then
                    gcs:send_text(2, "Rockblock: Cannot send message, no signal bars")
                end
                self.last_mavlink_send = now
            end
        end

        -- Append to buffer
        self.rxbuffer = self.rxbuffer .. rxstring

        -- Process complete lines
        while true do
            local cr_pos = self.rxbuffer:find("\r")
            if not cr_pos then break end

            local line = self.rxbuffer:sub(1, cr_pos - 1)
            self.rxbuffer = self.rxbuffer:sub(cr_pos + 1)

            if #line > 0 then
                self.process_line(line)
            end
        end
    end

    -- Initialize the modem
    function self.initialize_modem()
        -- Find and configure the serial port
        local port = RK9_SERPORT:get()
        if port then
            self.port = serial:find_serial(math.floor(port))
            if not self.port then
                gcs:send_text(2, "Rockblock: Failed to find serial port")
                return false
            end
        else
            gcs:send_text(2, "Rockblock: RK9_SERPORT parameter not set")
            return false
        end

        --Find and configure scripting serial ports for HL2 (43) messages
        port = RK9_SCRPORT:get()
        if port then
            self.scr_port = serial:find_simulated_device(43, math.floor(port))
            if not self.scr_port then
                gcs:send_text(2, 'Rockblock: could not find SCR_SDEV device')
                return false
            end
        else
            gcs:send_text(2, "Rockblock: RK9_SCRPORT parameter not set")
            return false
        end

        -- Configure serial port
        self.port:begin(230400)
        self.port:set_flow_control(0)

        -- Start modem initialization sequence
        self.send_get_command("apiVersion")

        return true
    end

    function self.clear_buffers()
        -- Clear the RX buffer
        self.rxbuffer = ""

        -- Clear the TX buffer
        self.txbuffer = nil
        self.cur_message = nil

        -- drain the serial port rx buffers
        self.port:readstring(self.port:available():toint())
        self.scr_port:readstring(self.scr_port:available():toint())

        if RK9_DEBUG:get() == 1 then
            gcs:send_text(6, "Rockblock: Buffers cleared")
        end
    end

    -- return the instance
    return self
end

-- Define the RockBlock interface
local rockblock = RockblockModem9704()

-- Main update function called periodically by ArduPilot
function HLSatcom()
    -- boot the modem
    if rockblock.state == ModemState.POWER0 then
        -- set I_EN to high to power the modem, if not already powered
        local pin = RK9_RELAY:get()
        if pin then
            relay:on(math.floor(pin) - 1) -- turn on I_EN. Note the indexing change from 1 to 0
        end
        gcs:send_text(5, "Rockblock: Powering modem")
        rockblock.state = ModemState.POWER1
        loop_delay_ms = 1000 -- wait 1 seconds for modem to power up
        return
    end

    if rockblock.state == ModemState.POWER1 then
        local pin = RK9_BOOTED:get()
        if pin then
            if gpio:read(math.floor(pin)) then
                rockblock.state = ModemState.BOOTED1 -- modem is booted
                gcs:send_text(5, "Rockblock: Modem booted")
                return
            end
        end
    end

    -- enable high latency mode, if desired
    if RK9_FORCEHL:get() == 1 and not gcs:get_high_latency_status() then
        gcs:enable_high_latency_connections(true)
    end

    -- Initialize the port on first run
    if not rockblock.port or not rockblock.scr_port then
        if not rockblock.initialize_modem() then
            -- raise an error to pcall to stop the script
            gcs:send_text(2, "Rockblock: Initialization failed, check serial port settings")
            loop_delay_ms = 20000 -- try again in 20 seconds
            return
        end
        rockblock.clear_buffers()
    end

    --- check if GCS telemetry has been lost for RK9_TIMEOUT sec (if param enabled)
    if RK9_FORCEHL:get() == 2 then
        -- link lost time = boot time - GCS last seen time
        link_lost_for = (millis()- gcs:last_seen()):toint()
        -- gcs:last_seen() is set to millis() during boot (on plane). 0 on rover/copter
        -- So if it's less than 10000 assume no GCS packet received since boot
        if link_lost_for > (RK9_TIMEOUT:get() * 1000) and not gcs:get_high_latency_status() and gcs:last_seen() > 10000 then
            gcs:enable_high_latency_connections(true)
        elseif link_lost_for < (RK9_TIMEOUT:get() * 1000) and gcs:get_high_latency_status() then
            gcs:enable_high_latency_connections(false)
        end
    end

    -- Read and process data from the serial port
    rockblock.modem_process()

    -- Handle state machine
    if not rockblock.cur_message then  -- Only proceed if no command is pending
        if rockblock.state == ModemState.BOOTED2 then
            rockblock.send_put_command("apiVersion", "\"active_version\": {\"major\": 1, \"minor\": 6, \"patch\": 1}")
            loop_delay_ms = 200
        elseif rockblock.state == ModemState.BOOTED3 then
            rockblock.send_get_command("hwInfo")
        elseif rockblock.state == ModemState.API_CONFIGURED then
            rockblock.send_put_command("simConfig", "\"interface\": \"internal\"")
        elseif rockblock.state == ModemState.SIM_CONFIGURED then
            rockblock.send_put_command("operationalState", "\"state\": \"active\"")
        elseif rockblock.state == ModemState.OPERATIONAL_CONFIGURED then
            local now = millis()
            if now - rockblock.last_status_check > rockblock.status_interval then
                rockblock.last_status_check = now
                rockblock.send_get_command("messageProvisioning")
                loop_delay_ms = 100 -- once modem is configured, speed up the loop to 100ms
            end
        elseif rockblock.state == ModemState.TOPIC_RECIEVED and gcs:get_high_latency_status() then
            -- Periodic constellation status check
            local now = millis()
            if now - rockblock.last_status_check > rockblock.status_interval and rockblock.cur_message == nil then
                rockblock.last_status_check = now
                rockblock.send_get_command("constellationState")
            end
        end
    end

    -- Trying to find modem
    if rockblock.state == ModemState.BOOTED1 then
        gcs:send_text(5, "Rockblock: Trying to detect modem")
        -- Start modem initialization sequence
        rockblock.send_get_command("apiVersion")
        loop_delay_ms = 5000 -- try again in 5 seconds
    end
end

-- wrapper around HLSatcom(). This calls HLSatcom() and if HLSatcom faults
-- then an error is displayed, but the script is not stopped
function protected_wrapper()
    local success, err = pcall(HLSatcom)
    if not success then
        gcs:send_text(2, "Rockblock: Internal Error: " .. err)
        -- when we fault we run the HLSatcom function again after 1s, slowing it
        -- down a bit so we don't flood the console with errors
        return protected_wrapper, 1000
    end
    return protected_wrapper, math.floor(loop_delay_ms)
end

-- start running HLSatcom loop
return protected_wrapper()
