Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- GeorgeBridge.lua -- keyboard/UDP control of the AH-64D George AI (device 87).
- --
- -- Runs in the DCS Export.lua environment. Listens on 127.0.0.1:7788 for
- -- single-line text datagrams and drives George's radial menu by issuing
- -- performClickableAction() on device 87.
- --
- -- The current menu mode is never assumed: it is read back from the live George
- -- overlay via list_indication(), so a command works no matter which wheel mode
- -- or submenu was last used. The read works with the wheel hidden because
- -- PrestonAI_page_common.lua carries an off-screen mirror element,
- -- mode_text_bridge, that does not inherit show_compass visibility. No command
- -- path presses show/hide, so the wheel stays down as it does under VAICOM.
- --
- -- Protocol (UDP, one command per datagram, reply sent to the sender):
- -- cmd <name> [count] execute a named command, optionally repeating its
- -- press `count` times (for the DEFN cycles)
- -- mode <NAME> switch the wheel to that mode and stop
- -- press <button> [short|long] raw button, e.g. "press down long"
- -- list [filter] reply with matching command names
- -- state reply with wheel / CMWS / flight state
- -- probe dump every list_indication() page to the log file
- -- eval <lua expr> evaluate an expression in the export environment
- -- cancel abort the running task
- local GEORGE_DEVICE = 87
- local CMWS_DEVICE = 80 -- devices["CMWS"]; driven directly, bypassing George
- local BTN = {
- control = 3001, -- Request control
- menu = 3002, -- Show/hide overlay (long press = open DEFN submenu)
- up = 3003,
- down = 3004,
- left = 3005, -- short press = cycle wheel mode
- right = 3006,
- mf = 3008, -- Multifunction / centre
- store = 3009,
- }
- -- CMWS_commands (command_defs.lua, base 3000).
- local CMWS = {
- pwr = 3001, -- PWR switch OFF/ON, arg 610
- arm_safe = 3005, -- flare squibs ARM/SAFE, arg 614
- byp_auto = 3007, -- operation BYPASS/AUTO, arg 616
- }
- local HOST, PORT = "127.0.0.1", 7788
- -- Timing. DCS treats >0.5 s as a long press (PrestonAI.lua: long_press_time).
- local SHORT_HOLD = 70
- local LONG_HOLD = 900
- local POST_PRESS = 170 -- settle time after releasing a button
- local MODE_SETTLE = 330 -- extra settle after a mode change before re-reading
- local MAX_MODE_STEPS = 8 -- bound on the mode-seek loop
- -- Cockpit argument ids (AH-64D clickabledata.lua).
- local ARG_APU = 406 -- APU, 0 off / 1 on
- local ARG_CMWS_PWR = 610 -- CMWS PWR switch, OFF/ON/TEST
- local ARG_CMWS_ARM_SAFE = 614 -- CMWS flare squibs, ARM/SAFE
- local ARG_CMWS_BYP_AUTO = 616 -- CMWS operation, BYPASS/AUTO
- local AIRBORNE_AGL = 1.5 -- metres; above this we treat the aircraft as flying
- -- Wheel mode labels. Keys are the normalised text of the overlay's mode_text
- -- element; values are our canonical name. Extend after running "probe".
- local MODE_ALIAS = {
- GND = "GND", GROUND = "GND",
- HOV = "HOV", HOVER = "HOV", HB = "HOV", HBOBUP = "HOV", BOBUP = "HOV",
- FLT = "FLT", FLIGHT = "FLT", FLGT = "FLT",
- CMBT = "CMBT", COMBAT = "CMBT", CBT = "CMBT", COMB = "CMBT",
- DEFN = "DEFN", DEF = "DEFN", DEFENSE = "DEFN", DEFENCE = "DEFN",
- }
- -- ---------------------------------------------------------------- logging
- local lfs_ok, lfs = pcall(require, "lfs")
- local LOG_PATH = lfs_ok and (lfs.writedir() .. "Logs\\GeorgeBridge.log") or nil
- local logfile = LOG_PATH and io.open(LOG_PATH, "w") or nil
- local function Log(fmt, ...)
- local msg = select("#", ...) > 0 and string.format(fmt, ...) or tostring(fmt)
- if logfile then
- logfile:write(os.date("%H:%M:%S "), msg, "\n")
- logfile:flush()
- end
- if log and log.write then log.write("GEORGE_BRIDGE", log.INFO, msg) end
- end
- -- ---------------------------------------------------------------- socket
- local socket_ok, socket = pcall(require, "socket")
- local sock, reply_to = nil, nil
- local function OpenSocket()
- if not socket_ok then
- Log("luasocket unavailable; bridge disabled")
- return
- end
- local s = socket.udp()
- s:setoption("reuseaddr", true)
- local ok, err = s:setsockname(HOST, PORT)
- if not ok then
- Log("cannot bind %s:%d (%s)", HOST, PORT, tostring(err))
- return
- end
- s:settimeout(0)
- sock = s
- Log("listening on %s:%d", HOST, PORT)
- end
- local function Reply(fmt, ...)
- local msg = select("#", ...) > 0 and string.format(fmt, ...) or tostring(fmt)
- Log("-> %s", msg)
- if sock and reply_to then
- pcall(function() sock:sendto(msg, reply_to.ip, reply_to.port) end)
- end
- end
- -- ---------------------------------------------------------------- reading
- -- list_indication() returns dashed blocks of "name\ntext". Parse to a table.
- local function ParseIndication(str)
- if type(str) ~= "string" or #str == 0 then return nil end
- local t, name, value = {}, nil, nil
- local function flush()
- if name then t[name] = value or "" end
- name, value = nil, nil
- end
- for line in ("\n" .. str):gmatch("\n([^\n]*)") do
- if line:match("^%-%-%-%-") then
- flush()
- elseif name == nil then
- if line ~= "" then name = line end
- else
- value = value and (value .. "\n" .. line) or line
- end
- end
- flush()
- if next(t) == nil then return nil end
- return t
- end
- local indicator_index = nil
- local function ReadOverlay()
- if not list_indication then return nil end
- if indicator_index then
- local ok, raw = pcall(list_indication, indicator_index)
- if not ok then return nil end
- local t = ParseIndication(raw)
- if t and (t.mode_text_bridge ~= nil or t.mode_text ~= nil) then return t end
- return nil
- end
- for i = 0, 40 do
- local ok, raw = pcall(list_indication, i)
- if ok and type(raw) == "string"
- and (raw:find("mode_text_bridge", 1, true) or raw:find("mode_text", 1, true)) then
- indicator_index = i
- Log("George overlay found at list_indication(%d)", i)
- return ParseIndication(raw)
- end
- end
- return nil
- end
- local function NormaliseMode(text)
- if type(text) ~= "string" then return nil end
- local key = text:upper():gsub("[^A-Z0-9]", "")
- if key == "" then return nil end
- return MODE_ALIAS[key] or key
- end
- -- mode_text_bridge is the off-screen mirror; it reports whether or not the
- -- wheel is drawn. mode_text is the unpatched fallback, readable only while the
- -- wheel is visible.
- local function CurrentMode()
- local overlay = ReadOverlay()
- if not overlay then return nil, false end
- local raw = overlay.mode_text_bridge
- local mirrored = raw ~= nil
- if raw == nil or raw == "" then raw = overlay.mode_text end
- return NormaliseMode(raw), mirrored
- end
- -- mode_text (unmirrored) only appears while the wheel is drawn, so its
- -- presence is an exact visibility test.
- local function WheelDrawn()
- local overlay = ReadOverlay()
- return overlay ~= nil and overlay.mode_text ~= nil
- end
- local function Arg(id)
- if not GetDevice then return nil end
- local ok, dev = pcall(GetDevice, 0)
- if not ok or not dev then return nil end
- local okv, v = pcall(function() return dev:get_argument_value(id) end)
- if okv then return v end
- return nil
- end
- local function AGL()
- if not LoGetAltitudeAboveGroundLevel then return nil end
- local ok, v = pcall(LoGetAltitudeAboveGroundLevel)
- if ok and type(v) == "number" then return v end
- return nil
- end
- local function Airborne()
- local agl = AGL()
- if agl == nil then return nil end
- return agl > AIRBORNE_AGL
- end
- local function IsApache()
- if not LoGetSelfData then return true end
- local ok, self_data = pcall(LoGetSelfData)
- if not ok or not self_data or not self_data.Name then return true end
- return self_data.Name:find("AH%-64D") ~= nil
- end
- local function Now()
- if LoGetModelTime then
- local ok, t = pcall(LoGetModelTime)
- if ok and type(t) == "number" then return t end
- end
- return os.clock()
- end
- -- ---------------------------------------------------------------- commands
- --
- -- Transcribed from VAICOM-Community
- -- VAICOM/Extensions/AICPG/CPGCommandHandler.cs. Two groups:
- --
- -- PILOT group -- what you command from the CP/G seat, where George flies.
- -- Each entry names the wheel mode it lives in; the bridge
- -- cycles to that mode first, reading back as it goes.
- -- CPG group -- what you command from the PILOT seat, where George runs
- -- the TADS and weapons. These have no mode gating at all,
- -- so they are plain presses.
- --
- -- press: "short", "long" (held 900 ms, past DCS's 0.5 s threshold), "hold"
- -- (long plus hold_ms, VAICOM's AddGeorgeLongHoldButton), or "action" (a single
- -- value 1.0 with no release, VAICOM's AddGeorgeAction).
- local function M(desc, mode, button, press, opts)
- local c = opts or {}
- c.desc, c.mode, c.button, c.press = desc, mode, button, press
- return c
- end
- local function R(desc, button, press, opts)
- local c = opts or {}
- c.desc, c.button, c.press = desc, button, press
- return c
- end
- local function D(desc, button, press, opts)
- local c = opts or {}
- c.desc, c.button, c.press, c.defn = desc, button, press, true
- return c
- end
- local function PANEL(desc, command, value, arg)
- return { desc = desc,
- set = { device = CMWS_DEVICE, command = command, value = value, arg = arg } }
- end
- local function SEQ(desc, steps) return { desc = desc, sequence = steps } end
- local COMMANDS = {
- -- ============================================================ raw wheel
- -- Either seat. No mode handling, exactly as pressing the hat yourself.
- show_hide = R("Show/hide the George overlay", BTN.menu, "short"),
- up = R("Wheel up", BTN.up, "short"),
- down = R("Wheel down", BTN.down, "short"),
- left = R("Wheel left (cycles mode)", BTN.left, "short"),
- right = R("Wheel right", BTN.right, "short"),
- center = R("Wheel select (centre)", BTN.mf, "short"),
- up_long = R("Wheel up, held", BTN.up, "long"),
- down_long = R("Wheel down, held", BTN.down, "long"),
- left_long = R("Wheel left, held", BTN.left, "long"),
- right_long = R("Wheel right, held", BTN.right, "long"),
- center_long = R("Wheel select, held", BTN.mf, "long"),
- control_request = R("Request control", BTN.control, "action"),
- store_target = R("Store target", BTN.store, "action"),
- -- ========================================================= menu modes
- mode_ground = { desc = "Select GND mode", seek = "GND" },
- mode_hover = { desc = "Select H-B mode", seek = "HOV" },
- mode_flight = { desc = "Select FLT mode", seek = "FLT" },
- mode_combat = { desc = "Select CMBT mode", seek = "CMBT" },
- mode_next = R("Step to the next wheel mode", BTN.left, "short"),
- mode_defense = { desc = "Open the DEFN submenu", defn_open = true },
- -- ===================================================== PILOT: GND menu
- apu_start = M("Start the APU", "GND", BTN.up, "short",
- { skip_if = { arg = ARG_APU, equals = 1 }, verify = { arg = ARG_APU, want = 1 } }),
- apu_stop = M("Stop the APU", "GND", BTN.up, "short",
- { skip_if = { arg = ARG_APU, equals = 0 }, verify = { arg = ARG_APU, want = 0 } }),
- apu_only = M("APU only", "GND", BTN.down, "short"),
- shutdown_engines = M("Shut down the engines", "GND", BTN.down, "short"),
- start_engines_idle = M("Start engines to idle", "GND", BTN.right, "short"),
- start_engines_fly = M("Start engines to fly", "GND", BTN.up, "short"),
- startup_full = M("Full start-up", "GND", BTN.up, "long"),
- shutdown_full = M("Full shutdown", "GND", BTN.down, "long"),
- -- GND long-Right toggles CMWS power. The silent cmws_off / cmws_on below
- -- do the same thing directly and absolutely; these ask George.
- cmws_power_toggle_george = M("Toggle CMWS power via George", "GND", BTN.right, "long",
- { verify = { arg = ARG_CMWS_PWR } }),
- takeoff = M("Take off", "GND", BTN.mf, "short",
- { modes = { "GND", "HOV", "DEFN" }, require_ground = true }),
- -- ===================================================== PILOT: FLT menu
- speed_up = M("Speed up", "FLT", BTN.up, "short"),
- slow_down = M("Slow down", "FLT", BTN.down, "short"),
- follow_waypoints = M("Follow waypoints", "FLT", BTN.right, "short"),
- set_airspeed_ref = M("Set airspeed reference", "FLT", BTN.mf, "short"),
- set_groundspeed_ref = M("Set groundspeed reference", "FLT", BTN.mf, "short"),
- increase_altitude = M("Increase altitude", "FLT", BTN.up, "hold", { hold_ms = 1000 }),
- decrease_altitude = M("Decrease altitude", "FLT", BTN.down, "hold", { hold_ms = 1000 }),
- come_left = M("Come left", "FLT", BTN.left, "hold", { hold_ms = 1000 }),
- come_right = M("Come right", "FLT", BTN.right, "hold", { hold_ms = 1000 }),
- set_radar_altitude = M("Set radar altitude hold", "FLT", BTN.mf, "long"),
- set_barometric_altitude = M("Set barometric altitude hold", "FLT", BTN.mf, "long"),
- -- ==================================================== PILOT: CMBT menu
- align_to_tads = M("Align to TADS", "CMBT", BTN.up, "short"),
- align_to_nts = M("Align to NTS", "CMBT", BTN.up, "short"),
- hold_position = M("Hold position", "CMBT", BTN.down, "short"),
- return_to_battle_position = M("Return to battle position", "CMBT", BTN.down, "short"),
- turn_to_ghs = M("Turn to GHS", "CMBT", BTN.right, "short"),
- orbit_overhead = M("Orbit overhead", "CMBT", BTN.up, "long"),
- break180 = M("Break 180", "CMBT", BTN.down, "long"),
- break_left = M("Break left", "CMBT", BTN.left, "long"),
- break_right = M("Break right", "CMBT", BTN.right, "long"),
- -- Add/delete share one long MF press; VAICOM presses it twice to replace an
- -- existing battle position, which needs state we do not read. One press.
- battle_position_toggle = M("Add or delete a battle position", "CMBT", BTN.mf, "long"),
- mask_position = M("Mask position", "CMBT", BTN.mf, "short",
- { modes = { "CMBT", "HOV", "DEFN" }, require_airborne = true }),
- -- ===================================================== PILOT: H-B menu
- hover_up_10ft = M("Come up 10 feet", "HOV", BTN.up, "short"),
- hover_down_10ft = M("Come down 10 feet", "HOV", BTN.down, "short"),
- hover_forward = M("Move forwards", "HOV", BTN.up, "hold", { hold_ms = 1000 }),
- hover_back = M("Move back", "HOV", BTN.down, "hold", { hold_ms = 1000 }),
- hover_left = M("Move left", "HOV", BTN.left, "hold", { hold_ms = 1000 }),
- hover_right = M("Move right", "HOV", BTN.right, "hold", { hold_ms = 1000 }),
- -- ==================================================== PILOT: DEFN menu
- -- Toggles, so each reads its cockpit argument first and presses nothing if
- -- the switch is already where it should be.
- cmws_safe_george = D("CMWS safe, via George", BTN.up, "short",
- { arg = ARG_CMWS_ARM_SAFE, want = 0 }),
- cmws_arm_george = D("CMWS arm, via George", BTN.up, "short",
- { arg = ARG_CMWS_ARM_SAFE, want = 1 }),
- cmws_bypass_george = D("CMWS bypass, via George", BTN.down, "short",
- { arg = ARG_CMWS_BYP_AUTO, want = 0 }),
- cmws_auto_george = D("CMWS auto, via George", BTN.down, "short",
- { arg = ARG_CMWS_BYP_AUTO, want = 1 }),
- threat_warnings_toggle = D("Toggle threat warnings", BTN.down, "long"),
- -- The remaining DEFN items are cycles with no cockpit argument behind them,
- -- so there is nothing to read back and absolute selection is not possible
- -- yet. Each press advances one step; pass a count, e.g. "cmd evade_next 3".
- roe_next = D("Next rules of engagement (hold / return / free)", BTN.up, "long"),
- evade_next = D("Next evade mode (off / level / vertical / mask)", BTN.left, "long"),
- dispense_next = D("Next dispense mode (none / chaff / flares / both)", BTN.right, "long"),
- extlights_next = D("Next exterior light setting", BTN.right, "short"),
- -- ============================================== CMWS panel, direct
- -- Not George at all: absolute switch positions on device 80. Idempotent,
- -- silent, seat-independent, confirmed from the cockpit argument.
- cmws_safe = PANEL("CMWS flare squibs SAFE", CMWS.arm_safe, 0.0, ARG_CMWS_ARM_SAFE),
- cmws_arm = PANEL("CMWS flare squibs ARM", CMWS.arm_safe, 1.0, ARG_CMWS_ARM_SAFE),
- cmws_off = PANEL("CMWS power OFF", CMWS.pwr, 0.0, ARG_CMWS_PWR),
- cmws_on = PANEL("CMWS power ON", CMWS.pwr, 1.0, ARG_CMWS_PWR),
- cmws_bypass = PANEL("CMWS BYPASS", CMWS.byp_auto, 0.0, ARG_CMWS_BYP_AUTO),
- cmws_auto = PANEL("CMWS AUTO", CMWS.byp_auto, 1.0, ARG_CMWS_BYP_AUTO),
- -- ================================================== CPG: George as CP/G
- -- From the PILOT seat. No mode gating; the CP/G wheel has none.
- cpg_startup = R("George start-up", BTN.mf, "long"),
- cpg_shutdown = R("George shutdown", BTN.mf, "long"),
- cpg_area_search = R("Area search", BTN.mf, "long"),
- cpg_point_search = R("Point search", BTN.mf, "short"),
- cpg_phs_search = R("PHS search", BTN.up, "short"),
- cpg_tads_los = R("TADS line of sight", BTN.down, "long"),
- cpg_area_select = R("Area select", BTN.left, "long"),
- cpg_point_select = R("Point select", BTN.right, "long"),
- cpg_tads_sensor = R("Cycle TADS sensor", BTN.mf, "long"),
- cpg_tads_fov = R("Cycle TADS field of view", BTN.mf, "short"),
- cpg_tads_zoom_in = R("TADS zoom in", BTN.up, "long"),
- cpg_tads_zoom_out = R("TADS zoom out", BTN.down, "long"),
- cpg_adjust_aim = R("Adjust aim", BTN.mf, "long"),
- cpg_next_target = R("Next target", BTN.down, "short"),
- cpg_previous_target = R("Previous target", BTN.up, "short"),
- cpg_next_item = R("Next list item", BTN.down, "short"),
- cpg_previous_item = R("Previous list item", BTN.up, "short"),
- cpg_list_item_select = R("Select list item", BTN.right, "short"),
- cpg_exit_list = R("Exit list", BTN.left, "short"),
- cpg_target_list_zoom_in = R("Target list zoom in", BTN.up, "long"),
- cpg_target_list_zoom_out = R("Target list zoom out", BTN.down, "long"),
- cpg_target_list_filter = R("Target list filter", BTN.left, "long"),
- cpg_point_list_filter_mode = R("Point list filter mode", BTN.left, "long"),
- cpg_point_list_filter_threat = R("Point list filter threat", BTN.right, "long"),
- cpg_select_target = R("Select target", BTN.mf, "short"),
- cpg_track_target = R("Track target", BTN.right, "short"),
- cpg_last_found_target = R("Last found target", BTN.down, "long"),
- cpg_last_stored_target = R("Last stored target", BTN.mf, "short"),
- cpg_lase_target = R("Lase target", BTN.right, "short"),
- cpg_laser_on = R("Laser on", BTN.right, "short"),
- cpg_laser_off = R("Laser off", BTN.right, "short"),
- cpg_cleared_fire = R("Cleared to fire", BTN.mf, "short"),
- cpg_weapons_free = R("Weapons free", BTN.up, "long"),
- cpg_hold_fire = R("Hold fire", BTN.up, "long"),
- cpg_next_weapon = R("Next weapon", BTN.left, "short"),
- cpg_next_missile = R("Next missile type", BTN.left, "long"),
- cpg_next_rocket = R("Next rocket type", BTN.left, "long"),
- cpg_missile_traj = R("Missile trajectory", BTN.right, "long"),
- cpg_burst_limit = R("Gun burst limit", BTN.right, "short"),
- cpg_rocket_quantity = R("Rocket quantity", BTN.right, "short"),
- cpg_lobl = R("LOBL", BTN.right, "short"),
- cpg_loal = R("LOAL", BTN.right, "short"),
- -- CP/G macros, button-for-button from CPGCommandHandler.cs. Each step is
- -- {button, press, post-delay ms}; the trailing menu press hides the list.
- cpg_macro_next_search = SEQ("Next area search", {
- { BTN.left, "long", 120 }, { BTN.down, "short", 80 },
- { BTN.right, "short", 80 }, { BTN.menu, "short", 0 } }),
- cpg_macro_previous_search = SEQ("Previous area search", {
- { BTN.left, "long", 200 }, { BTN.up, "short", 150 },
- { BTN.right, "short", 80 }, { BTN.menu, "short", 0 } }),
- cpg_macro_next_point = SEQ("Next point search", {
- { BTN.right, "long", 200 }, { BTN.down, "short", 150 },
- { BTN.right, "short", 80 }, { BTN.menu, "short", 0 } }),
- cpg_macro_previous_point = SEQ("Previous point search", {
- { BTN.right, "long", 200 }, { BTN.up, "short", 150 },
- { BTN.right, "short", 80 }, { BTN.menu, "short", 0 } }),
- cpg_macro_track_engage = SEQ("Track and engage current target", {
- { BTN.right, "short", 100 }, { BTN.mf, "short", 100 } }),
- cpg_macro_add_two_targets = SEQ("Add and track the top 2 targets", {
- { BTN.mf, "short", 100 }, { BTN.down, "short", 100 },
- { BTN.mf, "short", 100 }, { BTN.right, "short", 0 } }),
- cpg_macro_add_three_targets = SEQ("Add and track the top 3 targets", {
- { BTN.mf, "short", 100 }, { BTN.down, "short", 100 },
- { BTN.mf, "short", 100 }, { BTN.down, "short", 100 },
- { BTN.mf, "short", 100 }, { BTN.right, "short", 0 } }),
- cpg_macro_add_four_targets = SEQ("Add and track the top 4 targets", {
- { BTN.mf, "short", 100 }, { BTN.down, "short", 100 },
- { BTN.mf, "short", 100 }, { BTN.down, "short", 100 },
- { BTN.mf, "short", 100 }, { BTN.down, "short", 100 },
- { BTN.mf, "short", 100 }, { BTN.right, "short", 0 } }),
- }
- local BUTTON_BY_NAME = {
- control = BTN.control, menu = BTN.menu, up = BTN.up, down = BTN.down,
- left = BTN.left, right = BTN.right, mf = BTN.mf, centre = BTN.mf,
- center = BTN.mf, store = BTN.store,
- }
- -- ---------------------------------------------------------------- task runner
- -- Commands run as a coroutine so they can read state between button presses.
- -- Yielding a number parks the task for that many milliseconds.
- local task, wake_at, task_name = nil, 0, nil
- local function Wait(ms) coroutine.yield(ms) end
- local function RawPress(device, command, value)
- local ok, dev = pcall(GetDevice, device)
- if not ok or not dev or not dev.performClickableAction then
- return false, string.format("device %d unavailable", device)
- end
- local done, err = pcall(function() dev:performClickableAction(command, value) end)
- if not done then return false, tostring(err) end
- return true
- end
- local function Press(button, kind, hold_ms)
- if kind == "action" then
- local ok, err = RawPress(GEORGE_DEVICE, button, 1.0)
- if not ok then error("action failed: " .. tostring(err), 0) end
- Wait(POST_PRESS)
- return
- end
- local hold = SHORT_HOLD
- if kind == "long" then hold = LONG_HOLD
- elseif kind == "hold" then hold = LONG_HOLD + (hold_ms or 1000) end
- local ok, err = RawPress(GEORGE_DEVICE, button, 1.0)
- if not ok then error("press failed: " .. tostring(err), 0) end
- Wait(hold)
- RawPress(GEORGE_DEVICE, button, 0.0)
- Wait(POST_PRESS)
- end
- -- Cycle the wheel with short Left presses until the mode reports the target.
- -- Reading back after every press means no assumption about mode ordering, about
- -- which modes the current flight state offers, or about whether the DEFN
- -- submenu was left open. Nothing here presses the menu button, so the wheel is
- -- never raised: George processes hat commands whether or not it is drawn.
- local function SeekMode(target)
- for _ = 1, MAX_MODE_STEPS do
- local mode = CurrentMode()
- if mode == target then return true, mode end
- Press(BTN.left, "short")
- Wait(MODE_SETTLE)
- end
- return false, CurrentMode()
- end
- local function InList(value, list)
- if not list then return false end
- for _, v in ipairs(list) do if v == value then return true end end
- return false
- end
- -- Absolute panel switch, straight to the owning device. No George, no menu.
- local function RunSetCommand(name, set)
- local before = Arg(set.arg)
- local ok, err = RawPress(set.device, set.command, set.value)
- if not ok then
- Reply("error %s: %s", name, tostring(err))
- return
- end
- Wait(400)
- local after = Arg(set.arg)
- Reply("ok %s device=%d command=%d value=%s arg%d %s->%s",
- name, set.device, set.command, tostring(set.value), set.arg,
- tostring(before), tostring(after))
- end
- -- DEFN submenu: long-press the menu button to open it, press the item, short
- -- press to close. The menu button is also show/hide, so opening DEFN may leave
- -- the wheel drawn; we note whether it was drawn beforehand and put that back.
- local function RunDefnCommand(name, spec, count)
- local before = spec.arg and Arg(spec.arg) or nil
- if before ~= nil and spec.want ~= nil and before == spec.want then
- Reply("ok %s: already arg%d=%s, George not asked", name, spec.arg, tostring(before))
- return
- end
- local drawn_before = WheelDrawn()
- Press(BTN.menu, "long")
- Wait(MODE_SETTLE)
- local mode = CurrentMode()
- if mode ~= "DEFN" then
- Log("warning: %s expected DEFN after long menu press, mode=%s", name, tostring(mode))
- end
- for _ = 1, count do
- Press(spec.button, spec.press, spec.hold_ms)
- Wait(120)
- end
- Wait(MODE_SETTLE)
- Press(BTN.menu, "short")
- Wait(MODE_SETTLE)
- -- Restore the overlay to the visibility we found it in.
- local restored = true
- if WheelDrawn() ~= drawn_before then
- Press(BTN.menu, "short")
- Wait(MODE_SETTLE)
- restored = (WheelDrawn() == drawn_before)
- end
- Wait(400)
- if spec.arg then
- Reply("ok %s via DEFN x%d arg%d %s->%s mode=%s wheel_drawn=%s%s",
- name, count, spec.arg, tostring(before), tostring(Arg(spec.arg)),
- tostring(CurrentMode()), tostring(WheelDrawn()),
- restored and "" or " (could not restore wheel visibility)")
- else
- Reply("ok %s via DEFN x%d mode=%s wheel_drawn=%s%s",
- name, count, tostring(CurrentMode()), tostring(WheelDrawn()),
- restored and "" or " (could not restore wheel visibility)")
- end
- end
- local function RunSequence(name, steps)
- for _, step in ipairs(steps) do
- Press(step[1], step[2])
- if step[3] and step[3] > 0 then Wait(step[3]) end
- end
- Reply("ok %s (%d steps)", name, #steps)
- end
- local function RunNamedCommand(name, spec, count)
- if not IsApache() then
- Reply("error %s: not flying an AH-64D", name)
- return
- end
- if spec.set then return RunSetCommand(name, spec.set) end
- if spec.sequence then return RunSequence(name, spec.sequence) end
- if spec.skip_if then
- local v = Arg(spec.skip_if.arg)
- if v ~= nil and v == spec.skip_if.equals then
- Reply("ok %s: already arg%d=%s, nothing sent", name, spec.skip_if.arg, tostring(v))
- return
- end
- end
- if spec.require_airborne or spec.require_ground then
- local flying = Airborne()
- if flying == nil then
- Log("warning: %s could not read AGL, gate skipped", name)
- elseif spec.require_airborne and not flying then
- Reply("error %s: needs to be airborne (agl=%.1f m)", name, AGL() or 0)
- return
- elseif spec.require_ground and flying then
- Reply("error %s: needs to be on the ground (agl=%.1f m)", name, AGL() or 0)
- return
- end
- end
- if spec.defn_open then
- Press(BTN.menu, "long")
- Wait(MODE_SETTLE)
- Reply("ok mode_defense mode=%s wheel_drawn=%s", tostring(CurrentMode()), tostring(WheelDrawn()))
- return
- end
- if spec.defn then return RunDefnCommand(name, spec, count) end
- local mode, mirrored = CurrentMode()
- if spec.seek or spec.mode then
- if mode == nil then
- Reply("error %s: menu mode unreadable -- %s", name,
- mirrored and "mode_text_bridge is empty"
- or "mode_text_bridge missing, is PrestonAI_page_common.lua patched?")
- return
- end
- end
- if spec.seek then
- local reached, now_mode = SeekMode(spec.seek)
- if reached then Reply("ok %s mode=%s", name, spec.seek)
- else Reply("error %s: mode %s not reachable from %s (now %s)",
- name, spec.seek, mode, tostring(now_mode)) end
- return
- end
- if spec.mode and not InList(mode, spec.modes) then
- local start_mode = mode
- local reached, now_mode = SeekMode(spec.mode)
- if not reached then
- -- Put the wheel back where we found it rather than leaving it
- -- parked somewhere the pilot did not ask for.
- SeekMode(start_mode)
- Reply("error %s: mode %s not reachable from %s (now %s) -- not offered in this flight state",
- name, spec.mode, start_mode, tostring(now_mode))
- return
- end
- end
- for _ = 1, count do
- Press(spec.button, spec.press, spec.hold_ms)
- if count > 1 then Wait(120) end
- end
- if spec.verify then
- Wait(500)
- Reply("ok %s mode=%s arg%d=%s%s", name, tostring(CurrentMode()),
- spec.verify.arg, tostring(Arg(spec.verify.arg)),
- mirrored and "" or " (wheel-visible read only)")
- else
- Reply("ok %s%s%s", name,
- spec.mode and (" mode=" .. spec.mode) or "",
- mirrored and "" or " (wheel-visible read only)")
- end
- end
- local function StartTask(name, fn)
- if task then
- Reply("busy %s", tostring(task_name))
- return
- end
- task_name = name
- task = coroutine.create(fn)
- wake_at = 0
- end
- local function PumpTask()
- if not task then return end
- if Now() < wake_at then return end
- local ok, result = coroutine.resume(task)
- if not ok then
- Reply("error %s: %s", tostring(task_name), tostring(result))
- task, task_name = nil, nil
- return
- end
- if coroutine.status(task) == "dead" then
- task, task_name = nil, nil
- return
- end
- wake_at = Now() + (tonumber(result) or 0) / 1000
- end
- -- ---------------------------------------------------------------- handlers
- local function DoProbe()
- if not list_indication then
- Reply("error probe: list_indication unavailable")
- return
- end
- Log("---- probe: list_indication(0..40) ----")
- local found = {}
- for i = 0, 40 do
- local ok, raw = pcall(list_indication, i)
- if ok and type(raw) == "string" and #raw > 0 then
- found[#found + 1] = i
- Log("=== list_indication(%d) ===\n%s", i, raw)
- end
- end
- Log("---- args: apu406=%s cmws610=%s cmws614=%s cmws616=%s agl=%s ----",
- tostring(Arg(ARG_APU)), tostring(Arg(ARG_CMWS_PWR)),
- tostring(Arg(ARG_CMWS_ARM_SAFE)), tostring(Arg(ARG_CMWS_BYP_AUTO)), tostring(AGL()))
- Reply("ok probe: non-empty pages [%s] -> %s", table.concat(found, ","), tostring(LOG_PATH))
- end
- local function DoState()
- local mode, mirrored = CurrentMode()
- Reply('state indicator=%s mode=%s mirror=%s wheel_drawn=%s agl=%s apu=%s cmws_pwr=%s cmws_arm=%s cmws_auto=%s busy=%s',
- tostring(indicator_index), tostring(mode), tostring(mirrored),
- tostring(WheelDrawn()), tostring(AGL()), tostring(Arg(ARG_APU)),
- tostring(Arg(ARG_CMWS_PWR)), tostring(Arg(ARG_CMWS_ARM_SAFE)),
- tostring(Arg(ARG_CMWS_BYP_AUTO)), tostring(task_name))
- end
- local function DoList(filter)
- local names = {}
- for name in pairs(COMMANDS) do
- if filter == "" or name:find(filter, 1, true) then names[#names + 1] = name end
- end
- table.sort(names)
- Reply("ok %d command(s): %s", #names, table.concat(names, " "))
- end
- local function HandleLine(line)
- line = tostring(line or ""):gsub("^%s+", ""):gsub("%s+$", "")
- if line == "" then return end
- Log("<- %s", line)
- local verb, rest = line:match("^(%S+)%s*(.*)$")
- verb = verb:lower()
- if verb == "state" then
- DoState()
- elseif verb == "probe" then
- DoProbe()
- elseif verb == "list" then
- DoList(rest:lower())
- elseif verb == "cancel" then
- task, task_name = nil, nil
- Reply("ok cancel")
- elseif verb == "cmd" then
- local name, count_str = rest:lower():match("^(%S+)%s*(%S*)$")
- local spec = name and COMMANDS[name]
- if not spec then
- Reply("error unknown command %q -- try \"list\"", tostring(name))
- return
- end
- local count = tonumber(count_str) or 1
- if count < 1 or count > 12 then count = 1 end
- StartTask(name, function() RunNamedCommand(name, spec, count) end)
- elseif verb == "mode" then
- local target = NormaliseMode(rest)
- if not target then
- Reply("error mode: no target given")
- return
- end
- StartTask("mode " .. target, function()
- local reached, mode = SeekMode(target)
- if reached then Reply("ok mode %s", target)
- else Reply("error mode %s not reachable (now %s)", target, tostring(mode)) end
- end)
- elseif verb == "eval" then
- local fn, err = loadstring("return " .. rest)
- if not fn then
- Reply("error eval: %s", tostring(err))
- return
- end
- local ok, v = pcall(fn)
- Reply(ok and ("ok " .. tostring(v)) or ("error eval: " .. tostring(v)))
- elseif verb == "press" then
- local btn_name, kind = rest:match("^(%S+)%s*(%S*)$")
- local button = btn_name and BUTTON_BY_NAME[btn_name:lower()]
- if not button then
- Reply("error press: unknown button %q", tostring(btn_name))
- return
- end
- kind = (kind or ""):lower() == "long" and "long" or "short"
- StartTask("press " .. btn_name, function()
- Press(button, kind)
- Reply("ok press %s %s", btn_name, kind)
- end)
- else
- Reply("error unknown verb %q", verb)
- end
- end
- local function PumpSocket()
- if not sock then return end
- for _ = 1, 8 do
- local data, ip, port = sock:receivefrom()
- if not data then return end
- reply_to = { ip = ip, port = port }
- local ok, err = pcall(HandleLine, data)
- if not ok then Log("handler error: %s", tostring(err)) end
- end
- end
- -- ---------------------------------------------------------------- export hooks
- -- Chain onto whatever Export.lua already registered (WinWing, Tacview, ...).
- local prev_start = LuaExportStart
- local prev_frame = LuaExportAfterNextFrame
- local prev_stop = LuaExportStop
- LuaExportStart = function()
- OpenSocket()
- if prev_start then prev_start() end
- end
- LuaExportAfterNextFrame = function()
- PumpSocket()
- PumpTask()
- if prev_frame then prev_frame() end
- end
- LuaExportStop = function()
- if sock then pcall(function() sock:close() end) end
- sock = nil
- if logfile then logfile:close() logfile = nil end
- if prev_stop then prev_stop() end
- end
- Log("GeorgeBridge loaded")
- #!/usr/bin/env python3
- """Keyboard control of the AH-64D George AI, via GeorgeBridge.lua in DCS.
- The bridge does all the state handling: it reads George's live menu mode out of
- the cockpit overlay and cycles the wheel until the command's mode is actually
- selected. This program only maps hotkeys to command names and prints what the
- bridge reports back.
- python georgectl.py run start the hotkey listener
- python georgectl.py state print George's current menu mode
- python georgectl.py probe dump list_indication pages to the DCS log
- python georgectl.py cmd break180 fire one command
- python georgectl.py send "press left short"
- """
- from __future__ import annotations
- import argparse
- import socket
- import sys
- import time
- BRIDGE_ADDR = ("127.0.0.1", 7788)
- REPLY_TIMEOUT = 8.0
- # Hotkey -> bridge command, `keyboard` syntax.
- #
- # Everything George can be asked to do is listed below, commented out.
- # To enable one, uncomment the line and replace the "?" with the key you
- # want. A hotkey still containing "?" is skipped with a warning, so a
- # half-finished line cannot stop the listener from starting.
- #
- # Commands prefixed cpg_ are for the PILOT seat, where George runs the
- # TADS and weapons. Everything else is for the CP/G seat, where he flies.
- HOTKEYS = {
- # ---- raw wheel -----------------------------------------------------
- # "ctrl+alt+?": "cmd show_hide", # Show/hide the George overlay
- # "ctrl+alt+?": "cmd up", # Wheel up
- # "ctrl+alt+?": "cmd down", # Wheel down
- # "ctrl+alt+?": "cmd left", # Wheel left (cycles mode)
- # "ctrl+alt+?": "cmd right", # Wheel right
- # "ctrl+alt+?": "cmd center", # Wheel select (centre)
- # "ctrl+alt+?": "cmd up_long", # Wheel up, held
- # "ctrl+alt+?": "cmd down_long", # Wheel down, held
- # "ctrl+alt+?": "cmd left_long", # Wheel left, held
- # "ctrl+alt+?": "cmd right_long", # Wheel right, held
- # "ctrl+alt+?": "cmd center_long", # Wheel select, held
- # "ctrl+alt+?": "cmd control_request", # Request control
- # "ctrl+alt+?": "cmd store_target", # Store target
- # ---- menu modes ----------------------------------------------------
- # "ctrl+alt+?": "cmd mode_ground", # Select GND mode
- # "ctrl+alt+?": "cmd mode_hover", # Select H-B mode
- # "ctrl+alt+?": "cmd mode_flight", # Select FLT mode
- # "ctrl+alt+?": "cmd mode_combat", # Select CMBT mode
- # "ctrl+alt+?": "cmd mode_next", # Step to the next wheel mode
- # "ctrl+alt+?": "cmd mode_defense", # Open the DEFN submenu
- # ---- PILOT: GND menu -----------------------------------------------
- # "ctrl+alt+?": "cmd apu_start", # Start the APU
- # "ctrl+alt+?": "cmd apu_stop", # Stop the APU
- # "ctrl+alt+?": "cmd apu_only", # APU only
- # "ctrl+alt+?": "cmd shutdown_engines", # Shut down the engines
- # "ctrl+alt+?": "cmd start_engines_idle", # Start engines to idle
- # "ctrl+alt+?": "cmd start_engines_fly", # Start engines to fly
- # "ctrl+alt+?": "cmd startup_full", # Full start-up
- # "ctrl+alt+?": "cmd shutdown_full", # Full shutdown
- # "ctrl+alt+?": "cmd cmws_power_toggle_george", # Toggle CMWS power via George
- # "ctrl+alt+?": "cmd takeoff", # Take off
- # ---- PILOT: FLT menu -----------------------------------------------
- # "ctrl+alt+?": "cmd speed_up", # Speed up
- # "ctrl+alt+?": "cmd slow_down", # Slow down
- # "ctrl+alt+?": "cmd follow_waypoints", # Follow waypoints
- # "ctrl+alt+?": "cmd set_airspeed_ref", # Set airspeed reference
- # "ctrl+alt+?": "cmd set_groundspeed_ref", # Set groundspeed reference
- # "ctrl+alt+?": "cmd increase_altitude", # Increase altitude
- # "ctrl+alt+?": "cmd decrease_altitude", # Decrease altitude
- # "ctrl+alt+?": "cmd come_left", # Come left
- # "ctrl+alt+?": "cmd come_right", # Come right
- # "ctrl+alt+?": "cmd set_radar_altitude", # Set radar altitude hold
- # "ctrl+alt+?": "cmd set_barometric_altitude", # Set barometric altitude hold
- # ---- PILOT: CMBT menu ----------------------------------------------
- # "ctrl+alt+?": "cmd align_to_tads", # Align to TADS
- # "ctrl+alt+?": "cmd align_to_nts", # Align to NTS
- # "ctrl+alt+?": "cmd hold_position", # Hold position
- # "ctrl+alt+?": "cmd return_to_battle_position", # Return to battle position
- # "ctrl+alt+?": "cmd turn_to_ghs", # Turn to GHS
- # "ctrl+alt+?": "cmd orbit_overhead", # Orbit overhead
- "ctrl+alt+b": "cmd break180", # Break 180
- "ctrl+alt+left": "cmd break_left", # Break left
- "ctrl+alt+right": "cmd break_right", # Break right
- # "ctrl+alt+?": "cmd battle_position_toggle", # Add or delete a battle position
- # "ctrl+alt+?": "cmd mask_position", # Mask position
- # ---- PILOT: H-B menu -----------------------------------------------
- # "ctrl+alt+?": "cmd hover_up_10ft", # Come up 10 feet
- # "ctrl+alt+?": "cmd hover_down_10ft", # Come down 10 feet
- # "ctrl+alt+?": "cmd hover_forward", # Move forwards
- # "ctrl+alt+?": "cmd hover_back", # Move back
- # "ctrl+alt+?": "cmd hover_left", # Move left
- # "ctrl+alt+?": "cmd hover_right", # Move right
- # ---- PILOT: DEFN menu ----------------------------------------------
- # "ctrl+alt+?": "cmd cmws_safe_george", # CMWS safe, via George
- # "ctrl+alt+?": "cmd cmws_arm_george", # CMWS arm, via George
- # "ctrl+alt+?": "cmd cmws_bypass_george", # CMWS bypass, via George
- # "ctrl+alt+?": "cmd cmws_auto_george", # CMWS auto, via George
- # "ctrl+alt+?": "cmd threat_warnings_toggle", # Toggle threat warnings
- # "ctrl+alt+?": "cmd roe_next", # Next rules of engagement (hold / return / free)
- # "ctrl+alt+?": "cmd evade_next", # Next evade mode (off / level / vertical / mask)
- # "ctrl+alt+?": "cmd dispense_next", # Next dispense mode (none / chaff / flares / both)
- # "ctrl+alt+?": "cmd extlights_next", # Next exterior light setting
- # ---- CMWS panel, direct --------------------------------------------
- "ctrl+alt+c": "cmd cmws_safe", # CMWS flare squibs SAFE
- "ctrl+alt+shift+c": "cmd cmws_arm", # CMWS flare squibs ARM
- "ctrl+alt+o": "cmd cmws_off", # CMWS power OFF
- "ctrl+alt+shift+o": "cmd cmws_on", # CMWS power ON
- # "ctrl+alt+?": "cmd cmws_bypass", # CMWS BYPASS
- # "ctrl+alt+?": "cmd cmws_auto", # CMWS AUTO
- # ---- CPG: George as CP/G -------------------------------------------
- # "ctrl+alt+?": "cmd cpg_startup", # George start-up
- # "ctrl+alt+?": "cmd cpg_shutdown", # George shutdown
- # "ctrl+alt+?": "cmd cpg_area_search", # Area search
- # "ctrl+alt+?": "cmd cpg_point_search", # Point search
- # "ctrl+alt+?": "cmd cpg_phs_search", # PHS search
- # "ctrl+alt+?": "cmd cpg_tads_los", # TADS line of sight
- # "ctrl+alt+?": "cmd cpg_area_select", # Area select
- # "ctrl+alt+?": "cmd cpg_point_select", # Point select
- # "ctrl+alt+?": "cmd cpg_tads_sensor", # Cycle TADS sensor
- # "ctrl+alt+?": "cmd cpg_tads_fov", # Cycle TADS field of view
- # "ctrl+alt+?": "cmd cpg_tads_zoom_in", # TADS zoom in
- # "ctrl+alt+?": "cmd cpg_tads_zoom_out", # TADS zoom out
- # "ctrl+alt+?": "cmd cpg_adjust_aim", # Adjust aim
- # "ctrl+alt+?": "cmd cpg_next_target", # Next target
- # "ctrl+alt+?": "cmd cpg_previous_target", # Previous target
- # "ctrl+alt+?": "cmd cpg_next_item", # Next list item
- # "ctrl+alt+?": "cmd cpg_previous_item", # Previous list item
- # "ctrl+alt+?": "cmd cpg_list_item_select", # Select list item
- # "ctrl+alt+?": "cmd cpg_exit_list", # Exit list
- # "ctrl+alt+?": "cmd cpg_target_list_zoom_in", # Target list zoom in
- # "ctrl+alt+?": "cmd cpg_target_list_zoom_out", # Target list zoom out
- # "ctrl+alt+?": "cmd cpg_target_list_filter", # Target list filter
- # "ctrl+alt+?": "cmd cpg_point_list_filter_mode", # Point list filter mode
- # "ctrl+alt+?": "cmd cpg_point_list_filter_threat", # Point list filter threat
- # "ctrl+alt+?": "cmd cpg_select_target", # Select target
- # "ctrl+alt+?": "cmd cpg_track_target", # Track target
- # "ctrl+alt+?": "cmd cpg_last_found_target", # Last found target
- # "ctrl+alt+?": "cmd cpg_last_stored_target", # Last stored target
- # "ctrl+alt+?": "cmd cpg_lase_target", # Lase target
- # "ctrl+alt+?": "cmd cpg_laser_on", # Laser on
- # "ctrl+alt+?": "cmd cpg_laser_off", # Laser off
- # "ctrl+alt+?": "cmd cpg_cleared_fire", # Cleared to fire
- # "ctrl+alt+?": "cmd cpg_weapons_free", # Weapons free
- # "ctrl+alt+?": "cmd cpg_hold_fire", # Hold fire
- # "ctrl+alt+?": "cmd cpg_next_weapon", # Next weapon
- # "ctrl+alt+?": "cmd cpg_next_missile", # Next missile type
- # "ctrl+alt+?": "cmd cpg_next_rocket", # Next rocket type
- # "ctrl+alt+?": "cmd cpg_missile_traj", # Missile trajectory
- # "ctrl+alt+?": "cmd cpg_burst_limit", # Gun burst limit
- # "ctrl+alt+?": "cmd cpg_rocket_quantity", # Rocket quantity
- # "ctrl+alt+?": "cmd cpg_lobl", # LOBL
- # "ctrl+alt+?": "cmd cpg_loal", # LOAL
- # "ctrl+alt+?": "cmd cpg_macro_next_search", # Next area search
- # "ctrl+alt+?": "cmd cpg_macro_previous_search", # Previous area search
- # "ctrl+alt+?": "cmd cpg_macro_next_point", # Next point search
- # "ctrl+alt+?": "cmd cpg_macro_previous_point", # Previous point search
- # "ctrl+alt+?": "cmd cpg_macro_track_engage", # Track and engage current target
- # "ctrl+alt+?": "cmd cpg_macro_add_two_targets", # Add and track the top 2 targets
- # "ctrl+alt+?": "cmd cpg_macro_add_three_targets", # Add and track the top 3 targets
- # "ctrl+alt+?": "cmd cpg_macro_add_four_targets", # Add and track the top 4 targets
- # ---- bridge queries ------------------------------------------------
- "ctrl+alt+d": "state", # print mode / CMWS / flight state
- }
- class Bridge:
- def __init__(self, addr=BRIDGE_ADDR, timeout=REPLY_TIMEOUT):
- self.addr = addr
- self.timeout = timeout
- self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- self.sock.bind(("127.0.0.1", 0))
- self.sock.settimeout(timeout)
- def send(self, line: str) -> str | None:
- """Send one command and wait for the bridge's reply."""
- self.sock.sendto(line.encode("utf-8"), self.addr)
- deadline = time.monotonic() + self.timeout
- while time.monotonic() < deadline:
- try:
- data, _ = self.sock.recvfrom(4096)
- except socket.timeout:
- break
- except ConnectionResetError:
- # Nothing bound on 42087: DCS is not running, or Export.lua
- # did not load the bridge.
- return None
- return data.decode("utf-8", "replace").strip()
- return None
- def close(self):
- self.sock.close()
- def report(line: str, reply: str | None) -> bool:
- if reply is None:
- print(f"{line} -> no reply (is DCS running with the bridge loaded?)")
- return False
- print(f"{line} -> {reply}")
- return not reply.startswith("error")
- def run_hotkeys(bridge: Bridge) -> int:
- try:
- import keyboard
- except ImportError:
- print("The 'keyboard' package is required for hotkeys: pip install keyboard")
- return 1
- def make_handler(command: str):
- def handler():
- report(command, bridge.send(command))
- return handler
- # A line that was uncommented but not given a key would otherwise take the
- # whole listener down, so skip those and say which.
- unfinished = [c for c in HOTKEYS if "?" in c]
- for combo in unfinished:
- print(f" SKIPPED {combo!r} -> {HOTKEYS[combo]}: replace the '?' with a real key")
- for combo, command in HOTKEYS.items():
- if combo in unfinished:
- continue
- try:
- keyboard.add_hotkey(combo, make_handler(command), suppress=False)
- except Exception as exc:
- print(f" FAILED {combo:<20} {command} ({exc})")
- continue
- print(f" {combo:<20} {command}")
- print("\nListening. Ctrl+C to stop.")
- reply = bridge.send("state")
- report("state", reply)
- try:
- keyboard.wait()
- except KeyboardInterrupt:
- pass
- return 0
- def main(argv=None) -> int:
- parser = argparse.ArgumentParser(description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
- sub = parser.add_subparsers(dest="action", required=True)
- sub.add_parser("run", help="start the hotkey listener")
- sub.add_parser("state", help="print George's current menu mode")
- sub.add_parser("probe", help="dump list_indication pages to the DCS log")
- p_list = sub.add_parser("list", help="list the bridge's command names")
- p_list.add_argument("filter", nargs="?", default="")
- p_eval = sub.add_parser("eval", help="evaluate a lua expression in the export env")
- p_eval.add_argument("expr")
- p_cmd = sub.add_parser("cmd", help="fire one named George command")
- p_cmd.add_argument("name")
- p_send = sub.add_parser("send", help="send a raw bridge line")
- p_send.add_argument("line")
- args = parser.parse_args(argv)
- bridge = Bridge()
- try:
- if args.action == "run":
- return run_hotkeys(bridge)
- if args.action == "cmd":
- line = f"cmd {args.name}"
- elif args.action == "send":
- line = args.line
- elif args.action == "eval":
- line = f"eval {args.expr}"
- elif args.action == "list":
- line = f"list {args.filter}".strip()
- else:
- line = args.action
- return 0 if report(line, bridge.send(line)) else 1
- finally:
- bridge.close()
- if __name__ == "__main__":
- sys.exit(main())
Add Comment
Please, Sign In to add comment