RSS Git Download  Clone
Raw Blame History
-- "PMCO - Pilot Monitoring Callouts" FREEWARE LUA plugin
-- Version: forked from 2.3beta (13.05.2021), see Version below
-- by Nils Unger
-- port to ToLiss/XPlane 11 by Holger Teutsch (hotbso)

-- print(string.format("PMCO - PLANE_ICAO: %s", PLANE_ICAO))
print(string.format("############ PMCO - PLANE_ICAO: %s",  PLANE_ICAO))

-- ToLiss only (modified for FF A350)
if PLANE_ICAO == "A319" or PLANE_ICAO == "A20N" or PLANE_ICAO == "A321" or PLANE_ICAO == "A21N" or PLANE_ICAO == "A339" or PLANE_ICAO == "A359" then
--if PLANE_ICAO == "A319" or PLANE_ICAO == "A321" or PLANE_ICAO == "A21N" or PLANE_ICAO == "A346" then

-- =========================
local Version = "XPL 1.5"
-- =========================


-- ##################################################
-- ############ edit user options here ##############
-- ##################################################
local pV1 = 1 -- play PMCO V1 sound? 0 = never, 1 = yes
local V1_timing = 2 -- V1 will be announced at the speed of V1 - V1_timing. If you want V1 to be announced slightly before V1 is reached on the PFD, enter the number of kts.
local pCallFlapsOnGround = 0 -- play flaps callouts on the ground; 0 = no, 1 = yes
local pGearFlapDelay = 0 -- delay gear and flap callouts for this amount of time (in milliseconds)

local sound_set = "Copila" -- need Volume 0.4
local Volume = 0.3 -- volume of callouts
--local sound_set = "CyRanny" -- needs Volume 1.0
--local Volume = 1.0 -- volume of callouts

-- ##################################################
-- ##### end of user options, no editing below ######
-- ##################################################

-- flags and variables (NOT to edit by user!!!) -----
--local Volume = 0.4 -- default volume of callouts (soundset.cfg may override this) // v1.6 moved to user-editable section above
local bPlayV1 = pV1 -- decision to play PMCO V1 callout
if PLANE_ICAO == "A346" then bPlayV1 = 0 end  -- A346 has a hardwired internal V1 call
local PFD_delay = 650 -- milliseconds

-- flags and variables
local iBaseClock = 100 -- milliseconds
local iClockNo = -1 -- so we start with zero equals init
local iV1Select, iVrSelect = 0, 0 -- kts
local iIAS = 0 -- kts
local iALT = 0 -- ft
local iEng1_N1, iEng2_N1 = 0, 0 -- %
local iVertSpeed = 0 -- ft/min
local iAccelLateral = 0.0 -- ft/sec2
local iMaxDecel = 0.0
local iGS = 0.0 -- kts
local iThrustLeversOld = 0
local bThrustLeversReject = false
local iFlapRunning, iFlapLeverOld, iFlapDelay, iFlapLandingCfg = 0, 0, 2, 0
local sPhase = "init"
local blockFlexSet, blockToGaSet, blockClbSet = true, true, true
local block80kts = true
local block100kts, blockV1, blockRotate, blockLiftoff, blockPosClimb = true, true, true, true, true
local blockTouchdown, blockSpoilers, blockRevers, blockDecel, block70kts = true, true, true, true, true
local blockStop, blockStandUp, blockGo = true, true, true
local bCommitedToStop = false
local bMeasureDecel = false
local decel_ts
local lat0, lon0, alt0

-- need a table to save upvalues
local dr = {
    time_sec = XPLMFindDataRef("sim/time/total_running_time_sec"),
    ias = XPLMFindDataRef("sim/flightmodel/position/indicated_airspeed2"),
    gs = XPLMFindDataRef("sim/flightmodel/position/groundspeed"),
    THS = XPLMFindDataRef("AirbusFBW/PitchTrimPosition"),
    accel = XPLMFindDataRef("sim/flightmodel/position/local_az"),   -- +z points to tail
    spoiler_armed = XPLMFindDataRef("sim/cockpit2/controls/speedbrake_ratio"),
    on_ground = XPLMFindDataRef("sim/flightmodel/forces/fnrml_gear"),
    flap = XPLMFindDataRef("AirbusFBW/FlapLeverRatio"),
    spoilers = XPLMFindDataRef("AirbusFBW/SDSpoilerArray"),
    reversers = XPLMFindDataRef("AirbusFBW/ENGRevArray"),
    pitch = XPLMFindDataRef("sim/flightmodel/position/theta"),
    lat = XPLMFindDataRef("sim/flightmodel/position/latitude"),
    lon = XPLMFindDataRef("sim/flightmodel/position/longitude"),
    ra = XPLMFindDataRef("sim/flightmodel/position/y_agl"),
    N1 = XPLMFindDataRef("sim/flightmodel/engine/ENGN_N1_"),
    TLA = XPLMFindDataRef("AirbusFBW/throttle_input"),
-- VS dataref for FF A350 1.7.0 now same as ToLiss, airbus_qpac/* no longer used
	VS = XPLMFindDataRef("toliss_airbus/pfdoutputs/captain/vertical_speed")
}

-- dr.gear_lever = XPLMFindDataRef("1-sim/gearhandle") // seems to be wrong way round
if PLANE_ICAO == "A359" then 
    dr.gear_lever = XPLMFindDataRef("1-sim/gear/flag")
	dr.V1 = XPLMFindDataRef("1-sim/fms/perf/speed/v1")
	dr.VR = XPLMFindDataRef("1-sim/fms/perf/speed/vr")
else
    dr.gear_lever = XPLMFindDataRef("AirbusFBW/GearLever")
    dr.V1 = XPLMFindDataRef("toliss_airbus/performance/V1")
    dr.VR = XPLMFindDataRef("toliss_airbus/performance/VR")
end

-- dump dr table vals:
--for k, v in pairs(dr) do
--   print(string.format("PMCO dataref %s = %s", k, XPLMGetDataf(v)))
--end

local sounds = {
    ["100knots.wav"] = { pre = PFD_delay, post = 600 },
    ["70knots.wav"] = { pre = PFD_delay, post = 1000 },
    ["80knots.wav"] = { pre = 100, post = 100 },
    ["climbSet.wav"] = { pre = 1000, post = 1000 },
    ["decel.wav"] = { pre = 100, post = 700 },
    ["flaps1.wav"] = { pre = pGearFlapDelay, post = 1000 },
    ["flaps2.wav"] = { pre = pGearFlapDelay, post = 1000 },
    ["flaps3.wav"] = { pre = pGearFlapDelay, post = 1000 },
    ["flapsFull.wav"] = { pre = pGearFlapDelay, post = 1000 },
    ["flapsUp.wav"] = { pre = pGearFlapDelay, post = 1000 },
    ["flexSet.wav"] = { pre = 800, post = 1000 },  -- 800 = spool up delay
    ["gearDown.wav"] = { pre = 1000, post = 1000 },
    ["gearUp.wav"] = { pre = 1000, post = 1000 },
    ["go.wav"] = { pre = 500, post = 1000 },
    ["positiveClimb.wav"] = { pre = 0, post = 1000 },
    ["reverseGreen.wav"] = { pre = 10, post = 1000 },
    ["rotate.wav"] = { pre = PFD_delay, post = 1000 },
    ["spoilers.wav"] = { pre = 1200, post = 1700 },
    ["standUp.wav"] = { pre = 1000, post = 1000 },
    ["stop.wav"] = { pre = 700, post = 1200 },
    ["togaSet.wav"] = { pre = 1000, post = 1000 },
    ["v1.wav"] = { pre = PFD_delay, post = 1000 }
}

local sound_dir = SCRIPT_DIRECTORY .. "PMCOSounds/" .. sound_set .. "/"

-- initialize sounds
for s, sr in pairs(sounds) do
    local si = load_WAV_file(sound_dir .. s)
    sr.si = si
    set_sound_gain(si, Volume)
end

local wait_until_sound = 0
local wait_after_sound = 0
local sound_to_play

local next_tick = 0
local now = 0

-- used to reinit when spawned in the air
local function init_state()
    iClockNo = -1
    iThrustLeversOld = 0
    bThrustLeversReject = false
    iFlapRunning, iFlapLeverOld, iFlapDelay, iFlapLandingCfg = 0, 0, 2, 0
    sPhase = "init"
    blockFlexSet, blockToGaSet, blockClbSet = true, true, true
	block80kts = true
    block100kts, blockV1, blockRotate, blockLiftoff, blockPosClimb = true, true, true, true, true
    blockTouchdown, blockSpoilers, blockRevers, blockDecel, block70kts = true, true, true, true, true
    blockStop, blockStandUp, blockGo = true, true, true
    bCommitedToStop = false
    bMeasureDecel = false
    lat0 = nil
end


local log, schedule_sound   -- forward declaration

-- function declaration
-- this runs in the frame loop, efficient coding please
function PMCO_callouts() -- callouts logics function to play the callouts

    -- read & process raw sim data (with different clocks):
    -- ####################################################

    -- always try to feel the deceleration, even if waiting before sound

    -- measuring of max decel:
    -- #######################
    iAccelLateral = XPLMGetDataf(dr.accel) * 3.28084 -- lateral acceleration [ft/sec2]
    if bMeasureDecel and (iAccelLateral > iMaxDecel) then
      iMaxDecel = iAccelLateral -- log max decel from touchdown to 70kts
    end

    now = math.floor(XPLMGetDataf(dr.time_sec) * 1000)
    if now < wait_until_sound then return end
    if sound_to_play then
      play_sound(sound_to_play)
      sound_to_play = nil
    end
    if now < wait_after_sound then return end

    if now < next_tick then return end

    next_tick = now + iBaseClock
    iClockNo = iClockNo + 1
    if iClockNo > 4 then iClockNo = 1 end

    -- every clock (base clock = fast)
    iIAS = XPLMGetDataf(dr.ias) -- indicated air speed [kts]
    iGS = XPLMGetDataf(dr.gs) * 1.9438445 -- ground speed [kts]

     -- every even clock (2x base clock = middle)
    if (iClockNo % 2 == 0) then
      iVertSpeed = XPLMGetDataf(dr.VS) -- vertical speed [ft/min]

      iGNDSpoilersArmed = XPLMGetDataf(dr.spoiler_armed)-- speed brake lever
      local v = XPLMGetDatavi(dr.spoilers, 0, 1)
      iGNDSpoilersUp = v[0] -- ground spoilers (inner left representing all)
      iTHSval = XPLMGetDataf(dr.THS)      -- trimmable horizontal stabilizer: -4.0...+13.5

      -- ground spoilers armed
      if iGNDSpoilersArmed < 0 then
        iGNDSpoilersArmed = 1 -- yes
      else
        iGNDSpoilersArmed = 0 -- no
      end

    end -- 2x base clock

    -- every fourth clock (4x base clock = slow)
    if ((iClockNo == 0) or (iClockNo == 1)) then
      local N1 = XPLMGetDatavf(dr.N1, 0, 2)
      iEng1_N1 = N1[0] -- L engine N1 [%]
      iEng2_N1 = N1[1] -- R engine N1 [%]
      iPitch = XPLMGetDataf(dr.pitch) -- pitch [°]
      if XPLMGetDataf(dr.on_ground) > 1.0 then iOnGround = 1 else iOnGround = 0 end -- 1 = on the ground; 0 = airborne

      local alt = XPLMGetDataf(dr.ra) -- m
      local lat = XPLMGetDataf(dr.lat)
      local lon = XPLMGetDataf(dr.lon)

      -- test for teleportation = load of a situation
      if (lat0 ~= nil) then
        local R = 6371001.0 -- IUGG average radius of earth in m
        local d_1deg = R * math.pi / 180 -- distance 1 deg in m, ~ 60 sm

        -- simplified euclidian distance
        local x = (lon - lon0) * math.cos(math.rad((lat + lat0)/2)) * d_1deg
        local y = (lat - lat0) * d_1deg
        local z = alt - alt0
        local d = math.sqrt(x*x + y*y + z*z)
        local v = d / ((now - now0) * 0.001)
        if v > 340 then -- supersonic
          log("teleportation detected")
          init_state() -- reinit
          return
        end
      end

      lat0, lon0, alt0, now0 = lat, lon, alt, now

      iALT = alt * 3.28084 -- radar altitude [ft]

      local TLA = XPLMGetDatavf(dr.TLA, 0, 1)
      iThrustLevers = TLA[0] -- thrust levers (left lever representing both)
      iFlapLever = math.floor(XPLMGetDataf(dr.flap) * 4.0 + 0.5)
      iGearLever = XPLMGetDatai(dr.gear_lever) -- gear lever

      -- engines running
      if (iEng1_N1 < 15) and (iEng2_N1 < 15) then
        bEnginesRunning = false
      else
        bEnginesRunning = true
      end
      -- log(" N1: " .. iEng1_N1 .. ", " .. iEng2_N1)
      -- thrust lever position (detents)
      if iThrustLevers == 0 then sThrustLevers = "IDLE"
        elseif (iThrustLevers > 0) and (iThrustLevers < 0.69) then
          sThrustLevers = "MAN"
          bThrustLeversReject = false
        elseif math.abs(iThrustLevers - 0.7) < 0.05 then sThrustLevers = "CL"
        elseif math.abs(iThrustLevers - 0.88) < 0.05 then sThrustLevers = "FLX"
        elseif math.abs(iThrustLevers - 1.0) < 0.05 then sThrustLevers = "TOGA"
        elseif iThrustLevers < 0.0 then sThrustLevers = "REV"
        else
          sThrustLevers = "undefined"
      end
      -- log(" Levers: " .. iThrustLevers .. ", " .. sThrustLevers .. ", Ground: " .. iOnGround)
      if (iThrustLevers <= 0.0) and (iThrustLeversOld > 0) then
        if ((sPhase == "takeoff") or (sPhase == "touchAndGo")) then bThrustLeversReject = true end
      end
      iThrustLeversOld = iThrustLevers

      -- flap callout delay (to avoid multiple callouts)
      if iFlapRunning < iFlapDelay then iFlapRunning = iFlapRunning + 1 end -- increment delay timer
      if iFlapLever ~= iFlapLeverOld then -- flap lever was moved
        iFlapRunning = 0 -- reset delay
        iFlapLeverOld = iFlapLever -- save current value
      end
      -- -- ipc.display("iFlapRunning = " .. iFlapRunning)

      -- landing gear
      if iGearLever == 0 then
        iGearUp = 1
      else
        iGearUp = 0
      end
    end -- 4x base clock

    -- initialize toggle switches (first loop)
    -- #######################################
    if iClockNo == 0 then -- init only with first loop
      log("## initialize switch positions (first loop) ##")

      -- initialize gear lever
      if iGearUp == 1 then
        blockGearUp, blockGearDn = true, false -- allow "gear down" callout
        log("initialize gear: up")
      else
        blockGearDn, blockGearUp = true, false -- allow "gear up" callout
        log("initialize gear: down")
      end

      -- initialize flap lever
      if iFlapLever == 0 then
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = true, false, false, false, false
        log("initialize flaps: up")
      elseif iFlapLever == 1 then
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = false, true, false, false, false
        log("initialize flaps: 1")
      elseif iFlapLever == 2 then
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = false, false, true, false, false
        log("initialize flaps: 2")
      elseif iFlapLever == 3 then
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = false, false, false, true, false
        log("initialize flaps: 3")
      elseif iFlapLever == 4 then
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = false, false, false, false, true
        log("initialize flaps: full")
      end
    end -- first loop

    -- phases (main scenarios):
    -- ########################
    if ((iClockNo == 0) or (iClockNo == 2)) then -- not every cycle
      if not (sPhase == "enginesOff") and not bEnginesRunning and (iOnGround == 1) then -- engines off
        sPhase = "enginesOff"
        log("++ PMCO phase = " .. sPhase)
      end

      if not (sPhase == "taxiing") and bEnginesRunning and (iGS < 40) and ((sThrustLevers == "IDLE") or (sThrustLevers == "MAN")) then -- taxiing
        sPhase = "taxiing"
        log("++ PMCO phase = " .. sPhase)
        blockStop, blockSpoilers, blockRevers, blockDecel, block70kts = true, true, true, true, true
      elseif not ((sPhase == "takeoff") or (sPhase == "touchAndGo")) and bEnginesRunning and (iOnGround == 1) and ((sThrustLevers == "FLX") or (sThrustLevers == "TOGA")) then -- takeoff
        sPhase = "takeoff"
        log("++ PMCO phase = " .. sPhase .. " (" .. string.format("%.1f",iGS) .. "kts_GS and lever position '" .. sThrustLevers .. "')")
        blockFlexSet, blockToGaSet = false, false
		block80kts = false
        block100kts, blockV1, blockRotate, blockLiftoff = false, false, false, false
        iV1Select = XPLMGetDataf(dr.V1)
        if iV1Select == 0 then iV1Select = 1000 end -- if not set skip callout
        iVrSelect = XPLMGetDataf(dr.VR)
        if iVrSelect == 0 then iVrSelect = 1000 end
     elseif not (sPhase == "rejectedTakeoff") and ((sPhase == "takeoff") or (sPhase == "touchAndGo")) and (iGS > 30) and bThrustLeversReject then -- rejected takeoff
        sPhase = "rejectedTakeoff"
        log("++ PMCO phase = " .. sPhase)
        blockStop = false
        decel_ts = now
        if (iGS > 100) then
          blockDecel, blockRevers = false, false
          decel_ts = now + 7 * 1000
        end
        if (iGS > 75) then block70kts = false end
        blockFlexSet, blockToGaSet = true, true
		block80kts = true
        block100kts, blockV1, blockRotate = true, true, true
        blockStandUp, blockGo = true, true
        bMeasureDecel = true
        iMaxDecel = 0.0
      elseif not (sPhase == "climbOut") and ((sPhase == "takeoff") or (sPhase == "goAround") or (sPhase == "touchAndGo")) and (iVertSpeed > 500) then -- climb out
        sPhase = "climbOut"
        log("++ PMCO phase = " .. sPhase)
        blockPosClimb = false
        blockFlexSet, blockToGaSet = true, true
		block80kts = true
        block100kts, blockV1, blockRotate = true, true, true
        blockStandUp, blockGo = true, true
      elseif not (sPhase == "cruise") and ((sPhase == "climbOut") or (sPhase == "init")) and (iOnGround == 0) and (iThrustLevers <= 0.705) then -- cruise or pattern
        if not (sPhase == "init") and (sThrustLevers == "CL") then blockClbSet = false end -- to avoid the callout when loading the plug-in off the ground
        sPhase = "cruise"
        log("++ PMCO phase = " .. sPhase)
        blockLiftoff, blockPosClimb = true, true
      elseif not (sPhase == "landing") and (sPhase == "cruise") and (iGearUp == 0) and (iVertSpeed < -500) then -- landing
        sPhase = "landing"
        log("++ PMCO phase = " .. sPhase)
        blockTouchdown, blockSpoilers, blockRevers, blockDecel, block70kts = false, false, false, false, false
        blockClbSet = true
        bCommitedToStop = false
        bMeasureDecel = true
        iMaxDecel = 0.0
      elseif not (sPhase == "goAround") and (sPhase == "landing") and (sThrustLevers == "TOGA") then -- go-around
        sPhase = "goAround"
        log("++ PMCO phase = " .. sPhase)
        blockToGaSet = false
        blockTouchdown, blockSpoilers, blockRevers, blockDecel, block70kts = true, true, true, true, true
      elseif not (sPhase == "touchAndGo") and (sPhase == "landing") and (iOnGround == 1) and (iGNDSpoilersArmed == 0) and not bCommitedToStop then -- touch-and-go
        sPhase = "touchAndGo"
        log("++ PMCO phase = " .. sPhase)
        blockStandUp = false
        blockSpoilers, blockRevers, blockDecel, block70kts = true, true, true, true
        iFlapLandingCfg = iFlapLever -- save landing flaps
      end
    end



    -- callouts & logs:
    -- ################
    if ((iClockNo == 0) or (iClockNo == 3)) then -- non time-critical callouts
      if (iGearUp == 0) and not blockGearDn then -- "gear down" callout
        log("'gear down' callout (" .. string.format("%.0f",iIAS) .. "kts_IAS and " .. string.format("%.0f",iALT) .. "ft_ALT and " .. string.format("%.0f",iVertSpeed) .. "ft/min_VS)")
        blockGearDn, blockGearUp = true, false -- play only once
        schedule_sound("gearDown.wav")
        return
      end

      if (iGearUp == 1) and not blockGearUp then -- "gear up" callout
        blockGearUp, blockGearDn = true, false -- play only once
        log("'gear up' callout (" .. string.format("%.0f",iIAS) .. "kts_IAS and " .. string.format("%.0f",iALT) .. "ft_ALT and " .. string.format("%.0f",iVertSpeed) .. "ft/min_VS)")
        schedule_sound("gearUp.wav")
        return
      end

      if (iFlapLever == 0) and (iFlapRunning >= iFlapDelay) and not blockFlapsUp then -- "flaps up" callout
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = true, false, false, false, false
        if (iOnGround == 0) or (pCallFlapsOnGround == 1) then -- play on the ground?
          log("'flaps 0' callout (" .. string.format("%.0f",iIAS) .. "kts_IAS and " .. string.format("%.0f",iALT) .. "ft_ALT and " .. string.format("%.0f",iVertSpeed) .. "ft/min_VS)")
          schedule_sound("flapsUp.wav")
          return
        end
      end

      if (iFlapLever == 1) and (iFlapRunning >= iFlapDelay) and not blockFlaps1 then -- "flaps 1" callout
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = false, true, false, false, false
        if (iOnGround == 0) or (pCallFlapsOnGround == 1) then -- play on the ground?
          log("'flaps 1' callout (" .. string.format("%.0f",iIAS) .. "kts_IAS and " .. string.format("%.0f",iALT) .. "ft_ALT and " .. string.format("%.0f",iVertSpeed) .. "ft/min_VS)")
          schedule_sound("flaps1.wav")
          return
        end
      end

      if (iFlapLever == 2) and (iFlapRunning >= iFlapDelay) and not blockFlaps2 then -- "flaps 2" callout
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = false, false, true, false, false
        if ((iOnGround == 0) or (pCallFlapsOnGround == 1)) and not ((sPhase == "goAround") or (sPhase == "touchAndGo")) then -- don't play during go-around or touch-and-go
          log("'flaps 2' callout (" .. string.format("%.0f",iIAS) .. "kts_IAS and " .. string.format("%.0f",iALT) .. "ft_ALT and " .. string.format("%.0f",iVertSpeed) .. "ft/min_VS)")
          schedule_sound("flaps2.wav")
          return
        end
      end

      if (iFlapLever == 3) and (iFlapRunning >= iFlapDelay) and not blockFlaps3 then -- "flaps 3" callout
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = false, false, false, true, false
        if ((iOnGround == 0) or (pCallFlapsOnGround == 1)) and not ((sPhase == "goAround") or (sPhase == "touchAndGo")) then -- don't play during go-around or touch-and-go
          log("'flaps 3' callout (" .. string.format("%.0f",iIAS) .. "kts_IAS and " .. string.format("%.0f",iALT) .. "ft_ALT and " .. string.format("%.0f",iVertSpeed) .. "ft/min_VS)")
          schedule_sound("flaps3.wav")
          return
        end
      end

      if (iFlapLever == 4) and (iFlapRunning >= iFlapDelay) and not blockFlapsFull then -- "flaps full" callout
        blockFlapsUp, blockFlaps1, blockFlaps2, blockFlaps3, blockFlapsFull = false, false, false, false, true
        if (iOnGround == 0) or (pCallFlapsOnGround == 1) then -- play on the ground?
          log("'flaps full' callout (" .. string.format("%.0f",iIAS) .. "kts_IAS and " .. string.format("%.0f",iALT) .. "ft_ALT and " .. string.format("%.0f",iVertSpeed) .. "ft/min_VS)")
          schedule_sound("flapsFull.wav")
          return
        end
      end

      if (sThrustLevers == "FLX") and (iEng1_N1 >= 75.0) and not blockFlexSet then -- "flex set" callout
        blockFlexSet = true
        log("'flex set' callout (" .. string.format("%.1f",iEng1_N1) .. "%_N1 and " .. string.format("%.1f",iGS) .. "kts_GS)")
        schedule_sound("flexSet.wav")
        return
      end

      if (sThrustLevers == "TOGA") and (iEng1_N1 >= 80.0) and not blockToGaSet then -- "toga set" callout
        blockToGaSet, blockFlexSet = true, true
        blockGo, blockStandUp = true, true
        if (iOnGround == 1) then
          blockLiftoff, blockRotate = false, false
        end
        log("'toga set' callout (" .. string.format("%.1f",iEng1_N1) .. "%_N1 and " .. string.format("%.1f",iGS) .. "kts_GS and " .. string.format("%.1f",iIAS) .. "kts_IAS)")
        schedule_sound("togaSet.wav")
        return
      end

      if not blockClbSet then -- "climb set" callout
        blockClbSet = true
        log(string.format("'climb set' callout (%.0f kts_IAS and %.0f ft_ALT and %.0f ft/min_VS)", iIAS, iALT, iVertSpeed))
        schedule_sound("climbSet.wav")
        return
      end

      if not blockPosClimb then -- "positive climb" callout
        blockPosClimb = true
        log(string.format("'positive climb' callout (%.0f kts_IAS and %.0f ft_ALT and %.0f ft/min_VS)", iIAS, iALT, iVertSpeed))
        schedule_sound("positiveClimb.wav")
        return
      end

      if not blockStandUp and (iPitch <= 0.0) then -- "stand up" callout
        blockStandUp = true
        blockGo = false
        log("'stand up' callout (" .. string.format("%.0f",iGS) .. "kts_GS and " .. string.format("%.0f",iIAS) .. "kts_IAS and " .. string.format("%.1f",iPitch) .. "deg_Pitch and " .. string.format("%.2f",iTHSval) .. "THS)")
        schedule_sound("standUp.wav")
        return
      end

      if not ((sThrustLevers == "IDLE") or (sThrustLevers == "REV")) and (math.abs(iTHSval) < 0.8) and (iFlapLever < iFlapLandingCfg) and not blockGo then -- "Go" callout
        blockGo, blockStandUp = true, true
        blockToGaSet = false
        log("'go' callout (" .. string.format("%.0f",iGS) .. "kts_GS and " .. string.format("%.0f",iIAS) .. "kts_IAS and " .. string.format("%.1f",iPitch) .. "deg_Pitch and " .. string.format("%.2f",iTHSval) .. "THS and Flaps='" .. iFlapLever .. "')")
        schedule_sound("go.wav")
        return
      end

    end

    --
-- ######### copied 100kts call-out for 80kts:
    if (iGS >= 80.0) and not block80kts then -- "80kts" callout	  
      block80kts = true
      log(string.format("'80kts' callout (%.1f %%_N1 and %.1f kts_IAS)", iEng1_N1, iIAS))
      schedule_sound("80knots.wav")
      return
    end

    if (iIAS >= 100.0) and not block100kts then -- "100kts" callout
      block100kts = true
      log(string.format("'100kts' callout (%.1f %%_N1 and %.1f kts_IAS)", iEng1_N1, iIAS))
      schedule_sound("100knots.wav")
      return
    end

    if (bPlayV1 == 1)and (iIAS >= iV1Select) and not blockV1 then -- "V1" callout
      blockV1 = true
      log(string.format("'V1' callout (%.1f %%_N1 and %.1f kts_IAS)", iEng1_N1, iIAS))
      schedule_sound("v1.wav")
      return
    end

    if not blockStop then -- "stop" callout
      blockStop = true
      log("'stop' callout (" .. string.format("%.1f",iGS) .. "kts_GS and ".. string.format("%.1f",iIAS) .. "kts_IAS)")
      schedule_sound("stop.wav")
      return
    end

    if (iIAS >= iVrSelect) and not blockRotate then -- "rotate" callout
      blockRotate = true
      log(string.format("'rotate' callout (%.1f kts_IAS)", iIAS))
      schedule_sound("rotate.wav")
      return
    end

    if (iOnGround == 0) and not blockLiftoff then -- log liftoff
      blockLiftoff = true
      log("> liftoff! < (" .. string.format("%.1f",iEng1_N1) .. "%_N1 and " .. string.format("%.1f",iIAS) .. "kts_IAS and Flaps='" .. iFlapLever .. "')")
    end

    if (iOnGround == 1) and not blockTouchdown then -- log touchdown
      blockTouchdown = true
      log(string.format("> touchdown! < (%.0f kts_GS and %.0f kts_IAS and %.0f ft/min_VS and %.1f deg_Pitch and Flaps='%d')", iGS, iIAS, iVertSpeed, iPitch, iFlapLever))
      decel_ts = now + 7 * 1000 -- allow Rev Green before decel
    end

    if (iGNDSpoilersUp == 1) and not blockSpoilers then -- "spoilers" callout
      blockSpoilers = true
      log(string.format("'spoilers' callout (%.1f kts_GS and %.1f ft/sec2_accel)", iGS, iAccelLateral))
      schedule_sound("spoilers.wav")
      return
    end

    if (sThrustLevers == "REV") and not blockRevers then -- "reverse green" callout
      blockSpoilers, bCommitedToStop = true, true
      v = XPLMGetDatavi(dr.reversers, 0, 1)
      if v[0] == 2 then -- 2 = fully deployed, take rev 1 for both
          blockRevers = true
          decel_ts = now
          log(string.format("'reverse green' callout (%.1f kts_GS and %.1f ft/sec2_accel)", iGS, iAccelLateral))
          schedule_sound("reverseGreen.wav")
          return
      end
    end

    if (iMaxDecel >= 0.75 * 5.0) and (iOnGround == 1) and not blockDecel and (now > decel_ts) then -- "decel" callout
      blockDecel, blockRevers, blockSpoilers = true, true, true
      bCommitedToStop = true
      log(string.format("'decel' callout (%.1f kts_GS and %.1f ft/sec2_accel)", iGS, iAccelLateral))
      schedule_sound("decel.wav")
      return
    end

    if (iGS <= 70) and not block70kts then -- "70kts" callout
      block70kts, blockDecel, blockRevers, blockSpoilers = true, true, true, true
      bCommitedToStop = true
      bMeasureDecel = false -- stop measuring
      log(string.format("'70kts' callout (%.1f kts_GS and %.1f ft/sec2_accel, [%.1f max])", iGS, iAccelLateral, iMaxDecel))
      schedule_sound("70knots.wav")
      return
    end

end -- end of callout function


function log(str) -- custom log function
  local temp = os.date("*t", os.time())
  print(string.format("PMCO: %02d:%02d:%02d %s", temp.hour, temp.min, temp.sec, str))
end

function schedule_sound(wav)
    local sr = sounds[wav]
    wait_until_sound = now + sr.pre
    sound_to_play = sr.si
    wait_after_sound = now + sr.post
end

log(Version .. " - ToLiss detected")

-- ##### start of plugin code #####
do_every_frame("PMCO_callouts()") -- run the callouts function

end -- ToLiss only